From 6760e015c87d6e0956177edcd6fe32264d1646d2 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Fri, 31 Jul 2026 10:40:23 -0500 Subject: [PATCH 1/3] feat: octane-table adapter --- docs/config.json | 121 ++ docs/devtools.md | 18 +- .../reference/type-aliases/FlexRenderProps.md | 2 +- .../classes/FlexRenderComponentInstance.md | 10 +- .../reference/classes/FlexRenderDirective.md | 2 +- .../type-aliases/FlexRenderContent.md | 2 +- .../type-aliases/FlexRenderInputContent.md | 2 +- .../type-aliases/RenderableComponent.md | 2 +- .../reference/type-aliases/SubscribeSource.md | 2 +- .../ember/reference/classes/FlexRenderCell.md | 2 +- .../reference/classes/FlexRenderFooter.md | 2 +- .../reference/classes/FlexRenderHeader.md | 2 +- .../reference/functions/createTableHook.md | 51 +- .../ember/reference/functions/useTable.md | 62 +- .../type-aliases/FlexRenderContext.md | 2 +- .../reference/classes/SubscribeDirective.md | 12 +- .../lit/reference/classes/TableController.md | 35 +- .../lit/reference/functions/FlexRender-1.md | 6 +- .../reference/functions/createTableHook.md | 2 +- .../lit/reference/functions/flexRender.md | 8 +- docs/framework/lit/reference/index.md | 1 + .../interfaces/CreateTableHookResult.md | 14 +- .../reference/type-aliases/AppCellContext.md | 18 +- .../type-aliases/AppColumnDefBase.md | 2 +- .../type-aliases/AppColumnDefTemplate.md | 2 +- .../reference/type-aliases/AppColumnHelper.md | 10 +- .../type-aliases/AppDisplayColumnDef.md | 2 +- .../type-aliases/AppGroupColumnDef.md | 4 +- .../type-aliases/AppHeaderContext.md | 12 +- .../lit/reference/type-aliases/AppLitTable.md | 20 +- .../reference/type-aliases/BoundComponents.md | 2 +- .../reference/type-aliases/ComponentType.md | 2 +- .../type-aliases/CreateTableHookOptions.md | 2 +- .../reference/type-aliases/FlexRenderProps.md | 4 +- .../reference/type-aliases/LitRenderable.md | 24 + .../lit/reference/type-aliases/LitTable.md | 2 +- .../reference/type-aliases/SelectionSource.md | 2 +- .../lit/reference/variables/subscribe.md | 6 +- docs/framework/octane/guide/aggregation.md | 135 ++ docs/framework/octane/guide/cell-selection.md | 407 ++++++ .../framework/octane/guide/column-faceting.md | 325 +++++ .../octane/guide/column-filtering.md | 466 +++++++ .../framework/octane/guide/column-ordering.md | 197 +++ docs/framework/octane/guide/column-pinning.md | 195 +++ .../framework/octane/guide/column-resizing.md | 249 ++++ docs/framework/octane/guide/column-sizing.md | 169 +++ .../octane/guide/column-visibility.md | 182 +++ .../octane/guide/composable-tables.md | 354 +++++ .../framework/octane/guide/custom-features.md | 373 ++++++ docs/framework/octane/guide/expanding.md | 330 +++++ .../framework/octane/guide/fuzzy-filtering.md | 219 +++ .../octane/guide/global-filtering.md | 274 ++++ docs/framework/octane/guide/grouping.md | 197 +++ docs/framework/octane/guide/pagination.md | 284 ++++ docs/framework/octane/guide/row-pinning.md | 249 ++++ docs/framework/octane/guide/row-selection.md | 258 ++++ docs/framework/octane/guide/sorting.md | 559 ++++++++ docs/framework/octane/guide/table-context.md | 153 +++ docs/framework/octane/guide/table-state.md | 430 ++++++ docs/framework/octane/quick-start.md | 179 +++ .../reference/functions/FlexRender-1.md | 55 + .../reference/functions/createTableHook.md | 40 + .../functions/createTableHookContexts.md | 69 + .../octane/reference/functions/flexRender.md | 48 + .../octane/reference/functions/useTable.md | 40 + docs/framework/octane/reference/index.md | 56 + .../reference/interfaces/AppCellComponent.md | 83 ++ .../reference/interfaces/AppCellContext.md | 101 ++ .../interfaces/AppCellPropsWithSelector.md | 86 ++ .../interfaces/AppCellPropsWithoutSelector.md | 68 + .../reference/interfaces/AppColumnHelper.md | 142 ++ .../interfaces/AppHeaderComponent.md | 83 ++ .../reference/interfaces/AppHeaderContext.md | 71 + .../interfaces/AppHeaderPropsWithSelector.md | 86 ++ .../AppHeaderPropsWithoutSelector.md | 68 + .../reference/interfaces/AppTableComponent.md | 62 + .../interfaces/AppTablePropsWithSelector.md | 60 + .../AppTablePropsWithoutSelector.md | 30 + .../interfaces/CreateTableHookResult.md | 179 +++ .../interfaces/SubscribeComponent.md | 132 ++ .../SubscribePropsWithSourceIdentity.md | 50 + .../SubscribePropsWithSourceWithSelector.md | 63 + .../interfaces/SubscribePropsWithStore.md | 69 + .../reference/interfaces/TableHookContexts.md | 111 ++ .../type-aliases/AppColumnDefBase.md | 57 + .../type-aliases/AppColumnDefTemplate.md | 20 + .../type-aliases/AppDisplayColumnDef.md | 52 + .../type-aliases/AppGroupColumnDef.md | 58 + .../reference/type-aliases/AppOctaneTable.md | 117 ++ .../type-aliases/CreateTableHookOptions.md | 122 ++ .../reference/type-aliases/FlexRenderProps.md | 44 + .../reference/type-aliases/OctaneTable.md | 219 +++ .../reference/type-aliases/Renderable.md | 29 + .../reference/type-aliases/SubscribeProps.md | 29 + .../type-aliases/SubscribePropsWithSource.md | 29 + .../reference/type-aliases/SubscribeSource.md | 24 + .../type-aliases/SubscribeStaticChild.md | 23 + .../type-aliases/TableComponentType.md | 33 + .../octane/reference/variables/Subscribe.md | 12 + .../reference/functions/FlexRender-1.md | 2 +- .../preact/reference/functions/flexRender.md | 2 +- .../preact/reference/functions/useTable.md | 2 +- .../type-aliases/AppGroupColumnDef.md | 2 +- .../reference/type-aliases/FlexRenderProps.md | 4 +- .../reference/type-aliases/PreactTable.md | 2 +- .../reference/type-aliases/SubscribeProps.md | 2 +- .../type-aliases/SubscribePropsWithSource.md | 2 +- .../reference/type-aliases/SubscribeSource.md | 2 +- .../reference/index/functions/FlexRender-1.md | 2 +- .../reference/index/functions/useTable.md | 2 +- .../index/type-aliases/AppGroupColumnDef.md | 2 +- .../index/type-aliases/FlexRenderProps.md | 2 +- .../index/type-aliases/ReactTable.md | 2 +- .../index/type-aliases/SubscribeProps.md | 2 +- .../type-aliases/SubscribePropsWithSource.md | 2 +- .../index/type-aliases/SubscribeSource.md | 2 +- .../type-aliases/AppGroupColumnDef.md | 2 +- .../reference/type-aliases/FlexRenderProps.md | 2 +- .../type-aliases/AppGroupColumnDef.md | 2 +- .../reference/functions/createTableHook.md | 2 +- .../type-aliases/AppGroupColumnDef.md | 2 +- .../vue/reference/variables/AppFlexRender.md | 2 +- .../vue/reference/variables/FlexRender.md | 2 +- docs/guide/table-and-column-meta.md | 54 + docs/guide/tables.md | 24 + docs/guide/worker-row-models.md | 2 +- docs/installation.md | 5 + docs/overview.md | 53 +- .../index/functions/assignPrototypeAPIs.md | 6 +- .../index/functions/assignTableAPIs.md | 4 +- .../index/functions/buildHeaderGroups.md | 8 +- .../index/functions/callMemoOrStaticFn.md | 4 +- .../index/functions/constructCell.md | 4 +- .../index/functions/constructColumn.md | 6 +- .../index/functions/constructHeader.md | 4 +- .../reference/index/functions/constructRow.md | 12 +- .../index/functions/constructTable.md | 2 +- .../index/functions/getFunctionNameInfo.md | 2 +- .../index/functions/getInitialTableState.md | 2 +- .../index/functions/makeStateUpdater.md | 2 +- .../interfaces/ColumnDef_RowAggregation.md | 2 +- .../index/interfaces/Column_RowAggregation.md | 2 +- .../index/interfaces/CreatedFilterFn.md | 6 +- .../index/interfaces/CreatedSortFn.md | 4 +- docs/reference/index/interfaces/FilterFn.md | 6 +- .../index/interfaces/ResolvedAggregationFn.md | 2 +- docs/reference/index/interfaces/SortFn.md | 4 +- .../index/interfaces/TableFeature.md | 25 +- .../index/interfaces/Table_CellSelection.md | 2 +- .../index/interfaces/Table_Columns.md | 2 +- docs/reference/index/interfaces/Table_Core.md | 60 +- .../index/interfaces/Table_CoreProperties.md | 54 +- .../index/interfaces/Table_Internal.md | 46 +- .../reference/index/interfaces/Table_Table.md | 60 +- .../index/type-aliases/AccessorColumnDef.md | 2 +- .../type-aliases/AggregationFnListItem.md | 2 +- .../index/type-aliases/AggregationFnOption.md | 2 +- .../index/type-aliases/AggregationFnRef.md | 2 +- .../type-aliases/ColumnAggregationValue.md | 2 +- .../reference/index/type-aliases/ColumnDef.md | 2 +- .../ColumnResizingDefaultOptions.md | 2 +- docs/reference/index/type-aliases/DeepKeys.md | 2 +- .../type-aliases/ExtractAggregationFnKeys.md | 2 +- .../index/type-aliases/ExtractFilterFnKeys.md | 2 +- .../index/type-aliases/ExtractSortFnKeys.md | 2 +- .../index/type-aliases/FilterFnOption.md | 2 +- .../index/type-aliases/NonFeatureKeys.md | 2 +- .../index/type-aliases/SortFnOption.md | 2 +- .../type-aliases/StringOrTemplateHeader.md | 2 +- .../functions/aggregateColumnValue.md | 2 +- .../functions/cell_getIsAggregated.md | 2 +- .../functions/column_clearSorting.md | 2 +- .../functions/column_getAggregationFns.md | 2 +- .../functions/column_getAggregationValue.md | 2 +- .../functions/column_getAutoAggregationFn.md | 4 +- .../functions/column_getAutoFilterFn.md | 2 +- .../functions/column_getCanMultiSort.md | 2 +- .../functions/column_getCanSort.md | 2 +- .../functions/column_getFilterFn.md | 2 +- .../functions/column_getFirstSortDir.md | 2 +- .../functions/column_getIsSorted.md | 2 +- .../functions/column_getNextSortingOrder.md | 2 +- .../functions/column_getSortIndex.md | 2 +- .../column_getToggleSortingHandler.md | 2 +- .../functions/column_toggleSorting.md | 4 +- .../functions/formatAggregatedCellValue.md | 2 +- .../functions/header_getContext.md | 2 +- .../functions/header_getLeafHeaders.md | 2 +- .../functions/header_getSize.md | 2 +- .../functions/header_getStart.md | 2 +- .../functions/isRowSelected.md | 2 +- .../functions/isSubRowSelected.md | 2 +- .../functions/normalizeAggregationRows.md | 2 +- .../normalizeUniqueAggregationRows.md | 2 +- .../functions/row_getParentRow.md | 2 +- .../static-functions/functions/row_pin.md | 6 +- .../functions/row_toggleSelected.md | 4 +- .../functions/selectRowsFn.md | 2 +- .../functions/shouldAutoRemoveFilter.md | 4 +- .../functions/table_getAllColumns.md | 2 +- .../functions/table_getAllFlatColumns.md | 2 +- .../functions/table_getAllFlatColumnsById.md | 2 +- .../functions/table_getAllLeafColumns.md | 2 +- .../functions/table_getAllLeafColumnsById.md | 2 +- .../functions/table_getCenterTotalSize.md | 2 +- .../functions/table_getColumn.md | 4 +- .../functions/table_getEndTotalSize.md | 2 +- .../table_getFilteredSelectedRowModel.md | 2 +- .../functions/table_getFlatHeaders.md | 2 +- .../functions/table_getFocusedCell.md | 2 +- .../functions/table_getFooterGroups.md | 2 +- .../functions/table_getGlobalFilterFn.md | 2 +- .../table_getGroupedSelectedRowModel.md | 2 +- .../functions/table_getHeaderGroups.md | 2 +- .../functions/table_getLeafHeaders.md | 2 +- .../functions/table_getPinnedLeafColumns.md | 2 +- .../table_getPinnedVisibleLeafColumns.md | 2 +- .../functions/table_getRow.md | 4 +- .../functions/table_getRowId.md | 6 +- .../functions/table_getSelectedRowModel.md | 2 +- .../functions/table_getStartTotalSize.md | 2 +- .../functions/table_getTotalSize.md | 2 +- .../functions/table_mergeOptions.md | 2 +- .../functions/table_publishExternalState.md | 49 + .../static-functions/functions/table_reset.md | 2 +- .../functions/table_resetColumnSizing.md | 2 +- .../functions/table_selectCellRange.md | 4 +- .../functions/table_setColumnSizing.md | 2 +- .../functions/table_setFocusedCell.md | 4 +- .../functions/table_setOptions.md | 14 +- .../table_syncExternalStateToBaseAtoms.md | 28 +- .../table_toggleAllPageRowsSelected.md | 4 +- .../functions/table_toggleAllRowsSelected.md | 4 +- docs/reference/static-functions/index.md | 1 + examples/alpine/aggregation/vite.config.js | 2 +- .../alpine/basic-app-table/vite.config.js | 2 +- .../alpine/basic-create-table/vite.config.js | 2 +- .../basic-dynamic-columns/vite.config.js | 2 +- .../basic-external-atoms/vite.config.js | 2 +- .../basic-external-state/vite.config.js | 2 +- examples/alpine/cell-selection/vite.config.js | 2 +- .../alpine/column-ordering/vite.config.js | 2 +- .../column-pinning-split/vite.config.js | 2 +- .../column-pinning-sticky/vite.config.js | 2 +- examples/alpine/column-pinning/vite.config.js | 2 +- .../column-resizing-performant/vite.config.js | 2 +- .../alpine/column-resizing/vite.config.js | 2 +- examples/alpine/column-sizing/vite.config.js | 2 +- .../alpine/column-visibility/vite.config.js | 2 +- examples/alpine/custom-plugin/vite.config.js | 2 +- examples/alpine/expanding/vite.config.js | 2 +- .../filters-faceted-bucketed/vite.config.js | 2 +- .../alpine/filters-faceted/vite.config.js | 2 +- examples/alpine/filters/vite.config.js | 2 +- .../alpine/grouped-aggregation/vite.config.js | 2 +- examples/alpine/grouping/vite.config.js | 2 +- examples/alpine/header-groups/vite.config.js | 2 +- examples/alpine/pagination/vite.config.js | 2 +- examples/alpine/row-pinning/vite.config.js | 2 +- examples/alpine/row-selection/vite.config.js | 2 +- .../sorting-dynamic-data/vite.config.js | 2 +- examples/alpine/sorting/vite.config.js | 2 +- examples/alpine/sub-components/vite.config.js | 2 +- examples/angular/aggregation/package.json | 4 +- examples/angular/basic-app-table/package.json | 4 +- .../basic-dynamic-columns/package.json | 4 +- .../angular/basic-external-atoms/package.json | 4 +- .../angular/basic-external-state/package.json | 4 +- .../angular/basic-inject-table/package.json | 4 +- examples/angular/cell-selection/package.json | 4 +- examples/angular/column-ordering/package.json | 4 +- .../angular/column-pinning-split/package.json | 4 +- .../column-pinning-sticky/package.json | 4 +- examples/angular/column-pinning/package.json | 4 +- .../column-resizing-performant/package.json | 4 +- examples/angular/column-resizing/package.json | 4 +- examples/angular/column-sizing/package.json | 4 +- .../angular/column-visibility/package.json | 4 +- .../angular/composable-tables/package.json | 4 +- examples/angular/custom-plugin/package.json | 4 +- examples/angular/editable/package.json | 4 +- examples/angular/expanding/package.json | 4 +- .../filters-faceted-bucketed/package.json | 4 +- examples/angular/filters-faceted/package.json | 4 +- examples/angular/filters-fuzzy/package.json | 4 +- examples/angular/filters/package.json | 4 +- .../angular/grouped-aggregation/package.json | 4 +- examples/angular/grouping/package.json | 4 +- examples/angular/header-groups/package.json | 4 +- examples/angular/kitchen-sink/package.json | 4 +- examples/angular/pagination/package.json | 4 +- examples/angular/remote-data/package.json | 6 +- examples/angular/row-dnd/package.json | 4 +- examples/angular/row-pinning/package.json | 4 +- .../angular/row-selection-signal/package.json | 4 +- examples/angular/row-selection/package.json | 4 +- examples/angular/signal-input/package.json | 4 +- examples/angular/sorting/package.json | 4 +- examples/angular/sub-components/package.json | 4 +- .../angular/virtualized-columns/package.json | 4 +- .../package.json | 4 +- .../angular/virtualized-rows/package.json | 4 +- .../angular/with-tanstack-form/package.json | 4 +- .../angular/with-tanstack-query/package.json | 4 +- examples/ember/aggregation/vite.config.mjs | 2 +- .../ember/basic-app-table/vite.config.mjs | 2 +- .../basic-external-atoms/vite.config.mjs | 2 +- .../basic-external-state/vite.config.mjs | 2 +- examples/ember/basic-table/vite.config.mjs | 2 +- examples/ember/cell-selection/vite.config.mjs | 2 +- .../ember/column-ordering/vite.config.mjs | 2 +- .../column-pinning-split/vite.config.mjs | 2 +- .../column-pinning-sticky/vite.config.mjs | 2 +- examples/ember/column-pinning/vite.config.mjs | 2 +- .../vite.config.mjs | 2 +- .../ember/column-resizing/vite.config.mjs | 2 +- examples/ember/column-sizing/vite.config.mjs | 2 +- .../ember/column-visibility/vite.config.mjs | 2 +- examples/ember/custom-plugin/vite.config.mjs | 2 +- examples/ember/editable/vite.config.mjs | 2 +- examples/ember/expanding/vite.config.mjs | 2 +- .../filters-faceted-bucketed/vite.config.mjs | 2 +- .../ember/filters-faceted/vite.config.mjs | 2 +- examples/ember/filters-fuzzy/vite.config.mjs | 2 +- examples/ember/filters/vite.config.mjs | 2 +- .../ember/grouped-aggregation/vite.config.mjs | 2 +- examples/ember/grouping/vite.config.mjs | 2 +- examples/ember/header-groups/vite.config.mjs | 2 +- examples/ember/kitchen-sink/vite.config.mjs | 2 +- examples/ember/pagination/vite.config.mjs | 2 +- examples/ember/remote-data/vite.config.mjs | 2 +- examples/ember/row-dnd/vite.config.mjs | 2 +- examples/ember/row-pinning/vite.config.mjs | 2 +- examples/ember/row-selection/vite.config.mjs | 2 +- examples/ember/sorting/vite.config.mjs | 2 +- examples/ember/sub-components/vite.config.mjs | 2 +- examples/lit/aggregation/vite.config.js | 2 +- examples/lit/basic-app-table/vite.config.js | 2 +- .../lit/basic-dynamic-columns/vite.config.js | 2 +- .../lit/basic-external-atoms/vite.config.js | 2 +- .../lit/basic-external-state/vite.config.js | 2 +- examples/lit/basic-subscribe/vite.config.js | 2 +- .../lit/basic-table-controller/vite.config.js | 2 +- examples/lit/cell-selection/vite.config.js | 2 +- examples/lit/column-ordering/vite.config.js | 2 +- .../lit/column-pinning-split/vite.config.js | 2 +- .../lit/column-pinning-sticky/vite.config.js | 2 +- examples/lit/column-pinning/vite.config.js | 2 +- .../column-resizing-performant/vite.config.js | 2 +- examples/lit/column-resizing/vite.config.js | 2 +- examples/lit/column-visibility/vite.config.js | 2 +- examples/lit/composable-tables/vite.config.js | 2 +- examples/lit/expanding/vite.config.js | 2 +- .../filters-faceted-bucketed/vite.config.js | 2 +- examples/lit/filters-faceted/vite.config.js | 2 +- examples/lit/filters-fuzzy/vite.config.js | 2 +- examples/lit/filters/vite.config.js | 2 +- .../lit/grouped-aggregation/vite.config.js | 2 +- examples/lit/grouping/vite.config.js | 2 +- examples/lit/header-groups/vite.config.js | 2 +- examples/lit/kitchen-sink/src/main.ts | 2 +- examples/lit/kitchen-sink/vite.config.js | 2 +- examples/lit/pagination/vite.config.js | 2 +- examples/lit/row-pinning/vite.config.js | 2 +- examples/lit/row-selection/vite.config.js | 2 +- .../lit/sorting-dynamic-data/vite.config.js | 2 +- examples/lit/sorting/vite.config.js | 2 +- examples/lit/sub-components/vite.config.js | 2 +- .../lit/virtualized-columns/vite.config.js | 2 +- .../vite.config.js | 2 +- examples/lit/virtualized-rows/vite.config.js | 2 +- examples/octane/aggregation/index.html | 13 + examples/octane/aggregation/package.json | 24 + examples/octane/aggregation/src/index.css | 87 ++ examples/octane/aggregation/src/main.tsrx | 309 +++++ examples/octane/aggregation/src/makeData.ts | 21 + examples/octane/aggregation/src/vite-env.d.ts | 1 + .../aggregation/tests/e2e/smoke.spec.ts | 108 ++ examples/octane/aggregation/tsconfig.json | 22 + examples/octane/aggregation/vite.config.ts | 9 + .../octane/basic-dynamic-columns/index.html | 12 + .../octane/basic-dynamic-columns/package.json | 23 + .../basic-dynamic-columns/src/index.css | 365 +++++ .../basic-dynamic-columns/src/main.tsrx | 394 ++++++ .../basic-dynamic-columns/src/makeData.ts | 44 + .../basic-dynamic-columns/src/vite-env.d.ts | 1 + .../tests/e2e/smoke.spec.ts | 52 + .../basic-dynamic-columns/tsconfig.json | 22 + .../basic-dynamic-columns/vite.config.ts | 9 + .../octane/basic-external-atoms/index.html | 12 + .../octane/basic-external-atoms/package.json | 24 + .../octane/basic-external-atoms/src/index.css | 365 +++++ .../octane/basic-external-atoms/src/main.tsrx | 158 +++ .../basic-external-atoms/src/makeData.ts | 48 + .../basic-external-atoms/src/vite-env.d.ts | 1 + .../tests/e2e/smoke.spec.ts | 65 + .../octane/basic-external-atoms/tsconfig.json | 22 + .../basic-external-atoms/vite.config.ts | 9 + .../octane/basic-external-state/index.html | 12 + .../octane/basic-external-state/package.json | 23 + .../octane/basic-external-state/src/index.css | 365 +++++ .../octane/basic-external-state/src/main.tsrx | 163 +++ .../basic-external-state/src/makeData.ts | 48 + .../basic-external-state/src/vite-env.d.ts | 1 + .../tests/e2e/smoke.spec.ts | 73 + .../octane/basic-external-state/tsconfig.json | 22 + .../basic-external-state/vite.config.ts | 9 + examples/octane/basic-subscribe/index.html | 12 + examples/octane/basic-subscribe/package.json | 24 + examples/octane/basic-subscribe/src/index.css | 372 ++++++ examples/octane/basic-subscribe/src/main.tsrx | 412 ++++++ .../octane/basic-subscribe/src/makeData.ts | 50 + .../octane/basic-subscribe/src/vite-env.d.ts | 1 + .../basic-subscribe/tests/e2e/smoke.spec.ts | 53 + examples/octane/basic-subscribe/tsconfig.json | 22 + .../octane/basic-subscribe/vite.config.ts | 9 + .../octane/basic-use-app-table/index.html | 12 + .../octane/basic-use-app-table/package.json | 22 + .../octane/basic-use-app-table/src/index.css | 365 +++++ .../octane/basic-use-app-table/src/main.tsrx | 126 ++ .../basic-use-app-table/src/vite-env.d.ts | 1 + .../tests/e2e/smoke.spec.ts | 37 + .../octane/basic-use-app-table/tsconfig.json | 22 + .../octane/basic-use-app-table/vite.config.ts | 9 + examples/octane/basic-use-table/index.html | 12 + examples/octane/basic-use-table/package.json | 22 + examples/octane/basic-use-table/src/index.css | 365 +++++ examples/octane/basic-use-table/src/main.tsrx | 116 ++ .../octane/basic-use-table/src/vite-env.d.ts | 1 + .../basic-use-table/tests/e2e/smoke.spec.ts | 39 + examples/octane/basic-use-table/tsconfig.json | 22 + .../octane/basic-use-table/vite.config.ts | 9 + examples/octane/column-ordering/index.html | 13 + examples/octane/column-ordering/package.json | 23 + examples/octane/column-ordering/src/index.css | 374 ++++++ examples/octane/column-ordering/src/main.tsrx | 194 +++ .../octane/column-ordering/src/makeData.ts | 48 + .../octane/column-ordering/src/vite-env.d.ts | 1 + .../column-ordering/tests/e2e/smoke.spec.ts | 208 +++ examples/octane/column-ordering/tsconfig.json | 22 + .../octane/column-ordering/vite.config.ts | 9 + .../octane/column-pinning-split/index.html | 13 + .../octane/column-pinning-split/package.json | 23 + .../octane/column-pinning-split/src/index.css | 374 ++++++ .../octane/column-pinning-split/src/main.tsrx | 370 ++++++ .../column-pinning-split/src/makeData.ts | 48 + .../column-pinning-split/src/vite-env.d.ts | 1 + .../tests/e2e/smoke.spec.ts | 210 +++ .../octane/column-pinning-split/tsconfig.json | 22 + .../column-pinning-split/vite.config.ts | 9 + .../octane/column-pinning-sticky/index.html | 13 + .../octane/column-pinning-sticky/package.json | 23 + .../column-pinning-sticky/src/index.css | 396 ++++++ .../column-pinning-sticky/src/main.tsrx | 286 ++++ .../column-pinning-sticky/src/makeData.ts | 48 + .../column-pinning-sticky/src/vite-env.d.ts | 1 + .../tests/e2e/smoke.spec.ts | 198 +++ .../column-pinning-sticky/tsconfig.json | 22 + .../column-pinning-sticky/vite.config.ts | 9 + examples/octane/column-pinning/index.html | 13 + examples/octane/column-pinning/package.json | 23 + examples/octane/column-pinning/src/index.css | 374 ++++++ examples/octane/column-pinning/src/main.tsrx | 235 ++++ .../octane/column-pinning/src/makeData.ts | 48 + .../octane/column-pinning/src/vite-env.d.ts | 1 + .../column-pinning/tests/e2e/smoke.spec.ts | 228 ++++ examples/octane/column-pinning/tsconfig.json | 22 + examples/octane/column-pinning/vite.config.ts | 9 + .../column-resizing-performant/index.html | 13 + .../column-resizing-performant/package.json | 23 + .../column-resizing-performant/src/index.css | 400 ++++++ .../column-resizing-performant/src/main.tsrx | 238 ++++ .../src/makeData.ts | 48 + .../src/vite-env.d.ts | 1 + .../tests/e2e/smoke.spec.ts | 95 ++ .../column-resizing-performant/tsconfig.json | 22 + .../column-resizing-performant/vite.config.ts | 9 + examples/octane/column-resizing/index.html | 13 + examples/octane/column-resizing/package.json | 23 + examples/octane/column-resizing/src/index.css | 419 ++++++ examples/octane/column-resizing/src/main.tsrx | 345 +++++ .../octane/column-resizing/src/makeData.ts | 46 + .../octane/column-resizing/src/vite-env.d.ts | 1 + .../column-resizing/tests/e2e/smoke.spec.ts | 95 ++ examples/octane/column-resizing/tsconfig.json | 22 + .../octane/column-resizing/vite.config.ts | 9 + examples/octane/column-sizing/index.html | 13 + examples/octane/column-sizing/package.json | 23 + examples/octane/column-sizing/src/index.css | 419 ++++++ examples/octane/column-sizing/src/main.tsrx | 276 ++++ examples/octane/column-sizing/src/makeData.ts | 46 + .../octane/column-sizing/src/vite-env.d.ts | 1 + .../column-sizing/tests/e2e/smoke.spec.ts | 191 +++ examples/octane/column-sizing/tsconfig.json | 22 + examples/octane/column-sizing/vite.config.ts | 9 + examples/octane/column-visibility/index.html | 13 + .../octane/column-visibility/package.json | 23 + .../octane/column-visibility/src/index.css | 365 +++++ .../octane/column-visibility/src/main.tsrx | 168 +++ .../octane/column-visibility/src/makeData.ts | 46 + .../column-visibility/src/vite-env.d.ts | 1 + .../column-visibility/tests/e2e/smoke.spec.ts | 262 ++++ .../octane/column-visibility/tsconfig.json | 22 + .../octane/column-visibility/vite.config.ts | 9 + examples/octane/composable-tables/index.html | 13 + .../octane/composable-tables/package.json | 24 + .../src/components/cell-components.tsrx | 122 ++ .../src/components/header-components.tsrx | 66 + .../components/indeterminate-checkbox.tsrx | 37 + .../src/components/table-components.tsrx | 117 ++ .../composable-tables/src/hooks/table.ts | 115 ++ .../octane/composable-tables/src/index.css | 620 +++++++++ .../octane/composable-tables/src/main.tsrx | 446 +++++++ .../octane/composable-tables/src/makeData.ts | 77 ++ .../composable-tables/src/vite-env.d.ts | 1 + .../composable-tables/tests/e2e/smoke.spec.ts | 95 ++ .../octane/composable-tables/tsconfig.json | 22 + .../octane/composable-tables/vite.config.ts | 9 + examples/octane/custom-plugin/index.html | 13 + examples/octane/custom-plugin/package.json | 23 + examples/octane/custom-plugin/src/index.css | 373 ++++++ examples/octane/custom-plugin/src/main.tsrx | 415 ++++++ examples/octane/custom-plugin/src/makeData.ts | 48 + .../octane/custom-plugin/src/vite-env.d.ts | 1 + .../custom-plugin/tests/e2e/smoke.spec.ts | 95 ++ examples/octane/custom-plugin/tsconfig.json | 22 + examples/octane/custom-plugin/vite.config.ts | 9 + examples/octane/expanding/index.html | 13 + examples/octane/expanding/package.json | 23 + examples/octane/expanding/src/index.css | 365 +++++ examples/octane/expanding/src/main.tsrx | 370 ++++++ examples/octane/expanding/src/makeData.ts | 48 + examples/octane/expanding/src/vite-env.d.ts | 1 + .../octane/expanding/tests/e2e/smoke.spec.ts | 228 ++++ examples/octane/expanding/tsconfig.json | 22 + examples/octane/expanding/vite.config.ts | 9 + .../filters-faceted-bucketed/index.html | 13 + .../filters-faceted-bucketed/package.json | 23 + .../filters-faceted-bucketed/src/index.css | 104 ++ .../filters-faceted-bucketed/src/main.tsrx | 400 ++++++ .../filters-faceted-bucketed/src/makeData.ts | 28 + .../src/vite-env.d.ts | 1 + .../tests/e2e/smoke.spec.ts | 128 ++ .../filters-faceted-bucketed/tsconfig.json | 22 + .../filters-faceted-bucketed/vite.config.ts | 9 + examples/octane/filters-faceted/index.html | 13 + examples/octane/filters-faceted/package.json | 24 + examples/octane/filters-faceted/src/index.css | 365 +++++ examples/octane/filters-faceted/src/main.tsrx | 376 ++++++ .../octane/filters-faceted/src/makeData.ts | 48 + .../octane/filters-faceted/src/vite-env.d.ts | 1 + .../filters-faceted/tests/e2e/smoke.spec.ts | 95 ++ examples/octane/filters-faceted/tsconfig.json | 22 + .../octane/filters-faceted/vite.config.ts | 9 + examples/octane/filters-fuzzy/index.html | 13 + examples/octane/filters-fuzzy/package.json | 24 + examples/octane/filters-fuzzy/src/index.css | 365 +++++ examples/octane/filters-fuzzy/src/main.tsrx | 371 ++++++ examples/octane/filters-fuzzy/src/makeData.ts | 50 + .../octane/filters-fuzzy/src/vite-env.d.ts | 1 + .../filters-fuzzy/tests/e2e/smoke.spec.ts | 95 ++ examples/octane/filters-fuzzy/tsconfig.json | 22 + examples/octane/filters-fuzzy/vite.config.ts | 9 + examples/octane/filters/index.html | 13 + examples/octane/filters/package.json | 24 + examples/octane/filters/src/index.css | 365 +++++ examples/octane/filters/src/main.tsrx | 353 +++++ examples/octane/filters/src/makeData.ts | 48 + examples/octane/filters/src/vite-env.d.ts | 1 + .../octane/filters/tests/e2e/smoke.spec.ts | 335 +++++ examples/octane/filters/tsconfig.json | 22 + examples/octane/filters/vite.config.ts | 9 + .../octane/grouped-aggregation/index.html | 13 + .../octane/grouped-aggregation/package.json | 23 + .../octane/grouped-aggregation/src/index.css | 365 +++++ .../octane/grouped-aggregation/src/main.tsrx | 310 +++++ .../grouped-aggregation/src/makeData.ts | 48 + .../grouped-aggregation/src/vite-env.d.ts | 1 + .../tests/e2e/smoke.spec.ts | 95 ++ .../octane/grouped-aggregation/tsconfig.json | 22 + .../octane/grouped-aggregation/vite.config.ts | 9 + examples/octane/grouping/index.html | 13 + examples/octane/grouping/package.json | 23 + examples/octane/grouping/src/index.css | 365 +++++ examples/octane/grouping/src/main.tsrx | 260 ++++ examples/octane/grouping/src/makeData.ts | 48 + examples/octane/grouping/src/vite-env.d.ts | 1 + .../octane/grouping/tests/e2e/smoke.spec.ts | 209 +++ examples/octane/grouping/tsconfig.json | 22 + examples/octane/grouping/vite.config.ts | 9 + examples/octane/header-groups/index.html | 13 + examples/octane/header-groups/package.json | 23 + examples/octane/header-groups/src/index.css | 365 +++++ examples/octane/header-groups/src/main.tsrx | 129 ++ examples/octane/header-groups/src/makeData.ts | 46 + .../octane/header-groups/src/vite-env.d.ts | 1 + .../header-groups/tests/e2e/smoke.spec.ts | 169 +++ examples/octane/header-groups/tsconfig.json | 22 + examples/octane/header-groups/vite.config.ts | 9 + examples/octane/kitchen-sink/index.html | 13 + examples/octane/kitchen-sink/package.json | 24 + examples/octane/kitchen-sink/src/index.css | 317 +++++ examples/octane/kitchen-sink/src/main.tsrx | 779 +++++++++++ examples/octane/kitchen-sink/src/makeData.ts | 48 + .../octane/kitchen-sink/src/vite-env.d.ts | 1 + .../kitchen-sink/tests/e2e/smoke.spec.ts | 95 ++ examples/octane/kitchen-sink/tsconfig.json | 22 + examples/octane/kitchen-sink/vite.config.ts | 9 + examples/octane/pagination/index.html | 13 + examples/octane/pagination/package.json | 24 + examples/octane/pagination/src/index.css | 369 ++++++ examples/octane/pagination/src/main.tsrx | 214 +++ examples/octane/pagination/src/makeData.ts | 48 + examples/octane/pagination/src/vite-env.d.ts | 1 + .../octane/pagination/tests/e2e/smoke.spec.ts | 278 ++++ examples/octane/pagination/tsconfig.json | 22 + examples/octane/pagination/vite.config.ts | 9 + examples/octane/row-pinning/index.html | 13 + examples/octane/row-pinning/package.json | 23 + examples/octane/row-pinning/src/index.css | 388 ++++++ examples/octane/row-pinning/src/main.tsrx | 441 ++++++ examples/octane/row-pinning/src/makeData.ts | 48 + examples/octane/row-pinning/src/vite-env.d.ts | 1 + .../row-pinning/tests/e2e/smoke.spec.ts | 228 ++++ examples/octane/row-pinning/tsconfig.json | 22 + examples/octane/row-pinning/vite.config.ts | 9 + examples/octane/row-selection/index.html | 13 + examples/octane/row-selection/package.json | 24 + examples/octane/row-selection/src/index.css | 372 ++++++ examples/octane/row-selection/src/main.tsrx | 385 ++++++ examples/octane/row-selection/src/makeData.ts | 50 + .../octane/row-selection/src/vite-env.d.ts | 1 + .../row-selection/tests/e2e/smoke.spec.ts | 301 +++++ examples/octane/row-selection/tsconfig.json | 22 + examples/octane/row-selection/vite.config.ts | 9 + examples/octane/sorting/.gitignore | 24 + examples/octane/sorting/index.html | 13 + examples/octane/sorting/package.json | 23 + examples/octane/sorting/src/index.css | 365 +++++ examples/octane/sorting/src/main.tsrx | 197 +++ examples/octane/sorting/src/makeData.ts | 60 + examples/octane/sorting/src/vite-env.d.ts | 1 + .../octane/sorting/tests/e2e/smoke.spec.ts | 339 +++++ examples/octane/sorting/tsconfig.json | 22 + examples/octane/sorting/vite.config.ts | 9 + examples/octane/sub-components/index.html | 13 + examples/octane/sub-components/package.json | 23 + examples/octane/sub-components/src/index.css | 365 +++++ examples/octane/sub-components/src/main.tsrx | 188 +++ .../octane/sub-components/src/makeData.ts | 48 + .../octane/sub-components/src/vite-env.d.ts | 1 + .../sub-components/tests/e2e/smoke.spec.ts | 168 +++ examples/octane/sub-components/tsconfig.json | 22 + examples/octane/sub-components/vite.config.ts | 9 + examples/preact/aggregation/src/main.tsx | 2 +- examples/preact/aggregation/vite.config.ts | 2 +- .../basic-dynamic-columns/vite.config.ts | 2 +- .../basic-external-atoms/vite.config.ts | 2 +- .../basic-external-state/vite.config.ts | 2 +- .../preact/basic-subscribe/vite.config.ts | 2 +- .../preact/basic-use-app-table/vite.config.ts | 2 +- .../preact/basic-use-table/vite.config.ts | 2 +- examples/preact/cell-selection/vite.config.ts | 2 +- .../preact/column-ordering/vite.config.ts | 2 +- .../column-pinning-split/vite.config.ts | 2 +- .../column-pinning-sticky/vite.config.ts | 2 +- examples/preact/column-pinning/vite.config.ts | 2 +- .../column-resizing-performant/vite.config.ts | 2 +- .../preact/column-resizing/vite.config.ts | 2 +- examples/preact/column-sizing/vite.config.ts | 2 +- .../preact/column-visibility/vite.config.ts | 2 +- .../preact/composable-tables/vite.config.ts | 2 +- examples/preact/custom-plugin/vite.config.ts | 2 +- examples/preact/expanding/vite.config.ts | 2 +- .../filters-faceted-bucketed/src/main.tsx | 2 +- .../filters-faceted-bucketed/vite.config.ts | 2 +- .../preact/filters-faceted/vite.config.ts | 2 +- examples/preact/filters-fuzzy/vite.config.ts | 2 +- examples/preact/filters/vite.config.ts | 2 +- .../preact/grouped-aggregation/vite.config.ts | 2 +- examples/preact/grouping/vite.config.ts | 2 +- examples/preact/header-groups/vite.config.ts | 2 +- examples/preact/kitchen-sink/src/main.tsx | 2 +- examples/preact/kitchen-sink/vite.config.ts | 2 +- examples/preact/pagination/vite.config.ts | 2 +- examples/preact/row-pinning/vite.config.ts | 2 +- examples/preact/row-selection/vite.config.ts | 2 +- examples/preact/sorting/vite.config.ts | 2 +- examples/preact/sub-components/vite.config.ts | 2 +- .../preact/with-tanstack-form/src/main.tsx | 4 +- .../preact/with-tanstack-form/vite.config.ts | 2 +- .../preact/with-tanstack-query/vite.config.ts | 2 +- examples/react/aggregation/vite.config.js | 2 +- .../basic-dynamic-columns/vite.config.js | 2 +- .../react/basic-external-atoms/vite.config.js | 2 +- .../react/basic-external-state/vite.config.js | 2 +- examples/react/basic-subscribe/vite.config.js | 2 +- .../react/basic-use-app-table/vite.config.js | 2 +- .../basic-use-legacy-table/vite.config.js | 2 +- examples/react/basic-use-table/vite.config.js | 2 +- examples/react/cell-selection/vite.config.js | 2 +- examples/react/column-dnd/vite.config.js | 2 +- examples/react/column-ordering/vite.config.js | 2 +- .../react/column-pinning-split/vite.config.js | 2 +- .../column-pinning-sticky/vite.config.js | 2 +- examples/react/column-pinning/vite.config.js | 2 +- .../column-resizing-performant/vite.config.js | 2 +- examples/react/column-resizing/vite.config.js | 2 +- examples/react/column-sizing/vite.config.js | 2 +- .../react/column-visibility/vite.config.js | 2 +- .../react/composable-tables/vite.config.js | 2 +- examples/react/custom-plugin/vite.config.js | 2 +- examples/react/expanding/vite.config.js | 2 +- .../filters-faceted-bucketed/src/main.tsx | 2 +- .../filters-faceted-bucketed/vite.config.js | 2 +- examples/react/filters-faceted/vite.config.js | 2 +- examples/react/filters-fuzzy/vite.config.js | 2 +- examples/react/filters/vite.config.js | 2 +- .../react/grouped-aggregation/vite.config.js | 2 +- examples/react/grouping/vite.config.js | 2 +- examples/react/header-groups/vite.config.js | 2 +- .../react/kitchen-sink-chakra-ui/src/main.tsx | 2 +- .../kitchen-sink-chakra-ui/vite.config.js | 2 +- .../react/kitchen-sink-hero-ui/src/main.tsx | 2 +- .../react/kitchen-sink-hero-ui/vite.config.js | 2 +- .../react/kitchen-sink-mantine/src/main.tsx | 2 +- .../react/kitchen-sink-mantine/vite.config.js | 2 +- .../kitchen-sink-material-ui/src/main.tsx | 2 +- .../kitchen-sink-material-ui/vite.config.js | 2 +- .../kitchen-sink-react-aria/src/main.tsx | 2 +- .../kitchen-sink-react-aria/vite.config.js | 2 +- .../kitchen-sink-shadcn-base/src/main.tsx | 2 +- .../kitchen-sink-shadcn-base/vite.config.js | 2 +- .../kitchen-sink-shadcn-radix/src/main.tsx | 2 +- .../kitchen-sink-shadcn-radix/vite.config.js | 2 +- examples/react/kitchen-sink/vite.config.ts | 2 +- examples/react/lib-chakra-ui/vite.config.js | 2 +- examples/react/lib-hero-ui/vite.config.js | 2 +- examples/react/lib-mantine/vite.config.js | 2 +- examples/react/lib-material-ui/vite.config.js | 2 +- examples/react/lib-react-aria/vite.config.js | 2 +- examples/react/lib-shadcn-base/vite.config.js | 2 +- .../react/lib-shadcn-radix/vite.config.js | 2 +- .../react/mantine-react-table/vite.config.js | 2 +- .../react/material-react-table/vite.config.js | 2 +- examples/react/pagination/vite.config.js | 2 +- examples/react/row-dnd/vite.config.js | 2 +- examples/react/row-pinning/vite.config.js | 2 +- examples/react/row-selection/vite.config.js | 2 +- examples/react/sorting/vite.config.js | 2 +- examples/react/spreadsheet/vite.config.js | 2 +- examples/react/sub-components/vite.config.js | 2 +- .../vite.config.js | 2 +- .../react/virtualized-columns/vite.config.js | 2 +- .../vite.config.js | 2 +- .../vite.config.js | 2 +- .../react/virtualized-rows/vite.config.js | 2 +- .../web-worker-row-models/vite.config.ts | 2 +- .../react/with-tanstack-form/src/main.tsx | 4 +- .../react/with-tanstack-form/vite.config.js | 2 +- .../react/with-tanstack-query/vite.config.js | 2 +- .../react/with-tanstack-router/vite.config.js | 2 +- examples/solid/aggregation/vite.config.ts | 2 +- examples/solid/basic-app-table/vite.config.ts | 2 +- .../basic-dynamic-columns/vite.config.ts | 2 +- .../solid/basic-external-atoms/vite.config.ts | 2 +- .../solid/basic-external-state/vite.config.ts | 2 +- examples/solid/basic-use-table/vite.config.ts | 2 +- examples/solid/cell-selection/vite.config.ts | 2 +- examples/solid/column-ordering/vite.config.ts | 2 +- .../solid/column-pinning-split/vite.config.ts | 2 +- .../column-pinning-sticky/vite.config.ts | 2 +- examples/solid/column-pinning/vite.config.ts | 2 +- .../column-resizing-performant/vite.config.ts | 2 +- examples/solid/column-resizing/vite.config.ts | 2 +- examples/solid/column-sizing/vite.config.ts | 2 +- .../solid/column-visibility/vite.config.ts | 2 +- .../solid/composable-tables/vite.config.ts | 2 +- examples/solid/expanding/vite.config.ts | 2 +- .../filters-faceted-bucketed/vite.config.ts | 2 +- examples/solid/filters-faceted/vite.config.ts | 2 +- examples/solid/filters-fuzzy/vite.config.ts | 2 +- examples/solid/filters/vite.config.ts | 2 +- .../solid/grouped-aggregation/vite.config.ts | 2 +- examples/solid/grouping/vite.config.ts | 2 +- examples/solid/header-groups/vite.config.ts | 2 +- examples/solid/kitchen-sink/vite.config.ts | 2 +- examples/solid/pagination/vite.config.ts | 2 +- examples/solid/row-pinning/vite.config.ts | 2 +- examples/solid/row-selection/vite.config.ts | 2 +- examples/solid/sorting/vite.config.ts | 2 +- examples/solid/sub-components/vite.config.ts | 2 +- .../solid/virtualized-columns/vite.config.ts | 2 +- .../vite.config.ts | 2 +- .../solid/virtualized-rows/vite.config.ts | 2 +- .../solid/with-tanstack-form/vite.config.ts | 2 +- .../solid/with-tanstack-query/vite.config.ts | 2 +- .../solid/with-tanstack-router/vite.config.ts | 2 +- examples/svelte/aggregation/vite.config.js | 2 +- .../svelte/basic-app-table/vite.config.js | 2 +- .../svelte/basic-create-table/vite.config.js | 2 +- .../basic-dynamic-columns/vite.config.js | 2 +- .../basic-external-atoms/vite.config.js | 2 +- .../basic-external-state/vite.config.js | 2 +- examples/svelte/basic-snippets/vite.config.js | 2 +- examples/svelte/cell-selection/vite.config.js | 2 +- .../svelte/column-ordering/vite.config.js | 2 +- .../column-pinning-split/vite.config.js | 2 +- .../column-pinning-sticky/vite.config.js | 2 +- examples/svelte/column-pinning/vite.config.js | 2 +- .../column-resizing-performant/vite.config.js | 2 +- .../svelte/column-resizing/vite.config.js | 2 +- examples/svelte/column-sizing/vite.config.js | 2 +- .../svelte/column-visibility/vite.config.js | 2 +- .../svelte/composable-tables/vite.config.js | 2 +- examples/svelte/expanding/vite.config.js | 2 +- examples/svelte/filtering/vite.config.js | 2 +- .../filters-faceted-bucketed/vite.config.js | 2 +- .../svelte/filters-faceted/vite.config.js | 2 +- examples/svelte/filters-fuzzy/vite.config.js | 2 +- .../svelte/grouped-aggregation/vite.config.js | 2 +- examples/svelte/grouping/vite.config.js | 2 +- examples/svelte/header-groups/vite.config.js | 2 +- examples/svelte/kitchen-sink/vite.config.js | 2 +- examples/svelte/pagination/vite.config.js | 2 +- examples/svelte/row-pinning/vite.config.js | 2 +- examples/svelte/row-selection/vite.config.js | 2 +- examples/svelte/sorting/vite.config.js | 2 +- examples/svelte/sub-components/vite.config.js | 2 +- .../svelte/virtualized-columns/vite.config.js | 2 +- .../src/App.svelte | 2 +- .../vite.config.js | 2 +- .../svelte/virtualized-rows/vite.config.js | 2 +- .../svelte/with-tanstack-form/vite.config.js | 2 +- .../svelte/with-tanstack-query/vite.config.js | 2 +- examples/vanilla/aggregation/vite.config.js | 2 +- examples/vanilla/basic/vite.config.js | 2 +- examples/vanilla/pagination/vite.config.js | 2 +- examples/vanilla/sorting/vite.config.js | 2 +- examples/vue/aggregation/vite.config.ts | 2 +- .../vue/basic-dynamic-columns/vite.config.ts | 2 +- .../vue/basic-external-atoms/vite.config.ts | 2 +- .../vue/basic-external-state/vite.config.ts | 2 +- .../vue/basic-use-app-table/vite.config.ts | 2 +- examples/vue/basic-use-table/vite.config.ts | 2 +- examples/vue/cell-selection/vite.config.ts | 2 +- examples/vue/column-ordering/vite.config.ts | 2 +- .../vue/column-pinning-split/vite.config.ts | 2 +- .../vue/column-pinning-sticky/vite.config.ts | 2 +- examples/vue/column-pinning/vite.config.ts | 2 +- .../column-resizing-performant/vite.config.ts | 2 +- examples/vue/column-resizing/vite.config.ts | 2 +- examples/vue/column-sizing/vite.config.ts | 2 +- examples/vue/column-visibility/vite.config.ts | 2 +- examples/vue/composable-tables/vite.config.ts | 2 +- examples/vue/expanding/vite.config.ts | 2 +- .../filters-faceted-bucketed/vite.config.ts | 2 +- examples/vue/filters-faceted/vite.config.ts | 2 +- examples/vue/filters-fuzzy/vite.config.ts | 2 +- examples/vue/filters/vite.config.ts | 2 +- .../vue/grouped-aggregation/vite.config.ts | 2 +- examples/vue/grouping/vite.config.ts | 2 +- examples/vue/header-groups/vite.config.ts | 2 +- examples/vue/kitchen-sink/vite.config.ts | 2 +- examples/vue/pagination/vite.config.ts | 2 +- examples/vue/row-pinning/vite.config.ts | 2 +- examples/vue/row-selection/vite.config.ts | 2 +- examples/vue/sorting/vite.config.ts | 2 +- examples/vue/sub-components/vite.config.ts | 2 +- .../vue/virtualized-columns/vite.config.ts | 2 +- .../vite.config.ts | 2 +- examples/vue/virtualized-rows/vite.config.ts | 2 +- .../vue/with-tanstack-form/vite.config.ts | 2 +- .../vue/with-tanstack-query/vite.config.ts | 2 +- knip.json | 14 + package.json | 3 +- packages/octane-table/README.md | 129 ++ packages/octane-table/docs-entry.d.ts | 6 + packages/octane-table/package.json | 69 + packages/octane-table/src/FlexRender.ts | 95 ++ packages/octane-table/src/Subscribe.tsrx | 63 + packages/octane-table/src/Subscribe.tsrx.d.ts | 8 + .../octane-table/src/createTableHook.tsrx | 466 +++++++ .../src/createTableHook.tsrx.d.ts | 30 + .../src/createTableHookContexts.ts | 143 ++ .../src/experimental-worker-plugin.ts | 1 + packages/octane-table/src/flex-render.ts | 1 + packages/octane-table/src/index.d.ts | 8 + packages/octane-table/src/index.ts | 19 + packages/octane-table/src/internal.ts | 22 + packages/octane-table/src/reactivity.ts | 17 + packages/octane-table/src/static-functions.ts | 1 + packages/octane-table/src/types.ts | 887 +++++++++++++ packages/octane-table/src/useTable.tsrx | 95 ++ packages/octane-table/src/useTable.tsrx.d.ts | 21 + .../tests/_fixtures/adapter-reactivity.tsrx | 183 +++ .../tests/_fixtures/app-table.tsrx | 135 ++ .../tests/_fixtures/features.tsrx | 305 +++++ .../tests/_fixtures/flex-render.tsrx | 73 + .../tests/_fixtures/ssr-table.tsrx | 50 + .../tests/_fixtures/table-basic.tsrx | 226 ++++ packages/octane-table/tests/_helpers.ts | 74 ++ .../conformance/adapter-reactivity.test.ts | 180 +++ .../tests/conformance/app-table.test.ts | 96 ++ .../tests/conformance/core.test.ts | 189 +++ .../tests/conformance/features.test.ts | 243 ++++ .../tests/conformance/flex-render.test.ts | 94 ++ .../tests/conformance/parity.test.ts | 28 + packages/octane-table/tests/ssr/ssr.test.ts | 19 + packages/octane-table/tests/test-setup.ts | 7 + .../tests/type-tests/public-surface.tsx | 100 ++ packages/octane-table/tsconfig.docs.json | 4 + packages/octane-table/tsconfig.json | 18 + packages/octane-table/vite.config.ts | 23 + packages/octane-table/vitest.ssr.config.ts | 21 + pnpm-lock.yaml | 1179 ++++++++++++++--- pnpm-workspace.yaml | 3 + scripts/config.js | 4 + scripts/generateDocs.js | 51 + vitest.workspace.mjs | 1 + 920 files changed, 44915 insertions(+), 955 deletions(-) create mode 100644 docs/framework/lit/reference/type-aliases/LitRenderable.md create mode 100644 docs/framework/octane/guide/aggregation.md create mode 100644 docs/framework/octane/guide/cell-selection.md create mode 100644 docs/framework/octane/guide/column-faceting.md create mode 100644 docs/framework/octane/guide/column-filtering.md create mode 100644 docs/framework/octane/guide/column-ordering.md create mode 100644 docs/framework/octane/guide/column-pinning.md create mode 100644 docs/framework/octane/guide/column-resizing.md create mode 100644 docs/framework/octane/guide/column-sizing.md create mode 100644 docs/framework/octane/guide/column-visibility.md create mode 100644 docs/framework/octane/guide/composable-tables.md create mode 100644 docs/framework/octane/guide/custom-features.md create mode 100644 docs/framework/octane/guide/expanding.md create mode 100644 docs/framework/octane/guide/fuzzy-filtering.md create mode 100644 docs/framework/octane/guide/global-filtering.md create mode 100644 docs/framework/octane/guide/grouping.md create mode 100644 docs/framework/octane/guide/pagination.md create mode 100644 docs/framework/octane/guide/row-pinning.md create mode 100644 docs/framework/octane/guide/row-selection.md create mode 100644 docs/framework/octane/guide/sorting.md create mode 100644 docs/framework/octane/guide/table-context.md create mode 100644 docs/framework/octane/guide/table-state.md create mode 100644 docs/framework/octane/quick-start.md create mode 100644 docs/framework/octane/reference/functions/FlexRender-1.md create mode 100644 docs/framework/octane/reference/functions/createTableHook.md create mode 100644 docs/framework/octane/reference/functions/createTableHookContexts.md create mode 100644 docs/framework/octane/reference/functions/flexRender.md create mode 100644 docs/framework/octane/reference/functions/useTable.md create mode 100644 docs/framework/octane/reference/index.md create mode 100644 docs/framework/octane/reference/interfaces/AppCellComponent.md create mode 100644 docs/framework/octane/reference/interfaces/AppCellContext.md create mode 100644 docs/framework/octane/reference/interfaces/AppCellPropsWithSelector.md create mode 100644 docs/framework/octane/reference/interfaces/AppCellPropsWithoutSelector.md create mode 100644 docs/framework/octane/reference/interfaces/AppColumnHelper.md create mode 100644 docs/framework/octane/reference/interfaces/AppHeaderComponent.md create mode 100644 docs/framework/octane/reference/interfaces/AppHeaderContext.md create mode 100644 docs/framework/octane/reference/interfaces/AppHeaderPropsWithSelector.md create mode 100644 docs/framework/octane/reference/interfaces/AppHeaderPropsWithoutSelector.md create mode 100644 docs/framework/octane/reference/interfaces/AppTableComponent.md create mode 100644 docs/framework/octane/reference/interfaces/AppTablePropsWithSelector.md create mode 100644 docs/framework/octane/reference/interfaces/AppTablePropsWithoutSelector.md create mode 100644 docs/framework/octane/reference/interfaces/CreateTableHookResult.md create mode 100644 docs/framework/octane/reference/interfaces/SubscribeComponent.md create mode 100644 docs/framework/octane/reference/interfaces/SubscribePropsWithSourceIdentity.md create mode 100644 docs/framework/octane/reference/interfaces/SubscribePropsWithSourceWithSelector.md create mode 100644 docs/framework/octane/reference/interfaces/SubscribePropsWithStore.md create mode 100644 docs/framework/octane/reference/interfaces/TableHookContexts.md create mode 100644 docs/framework/octane/reference/type-aliases/AppColumnDefBase.md create mode 100644 docs/framework/octane/reference/type-aliases/AppColumnDefTemplate.md create mode 100644 docs/framework/octane/reference/type-aliases/AppDisplayColumnDef.md create mode 100644 docs/framework/octane/reference/type-aliases/AppGroupColumnDef.md create mode 100644 docs/framework/octane/reference/type-aliases/AppOctaneTable.md create mode 100644 docs/framework/octane/reference/type-aliases/CreateTableHookOptions.md create mode 100644 docs/framework/octane/reference/type-aliases/FlexRenderProps.md create mode 100644 docs/framework/octane/reference/type-aliases/OctaneTable.md create mode 100644 docs/framework/octane/reference/type-aliases/Renderable.md create mode 100644 docs/framework/octane/reference/type-aliases/SubscribeProps.md create mode 100644 docs/framework/octane/reference/type-aliases/SubscribePropsWithSource.md create mode 100644 docs/framework/octane/reference/type-aliases/SubscribeSource.md create mode 100644 docs/framework/octane/reference/type-aliases/SubscribeStaticChild.md create mode 100644 docs/framework/octane/reference/type-aliases/TableComponentType.md create mode 100644 docs/framework/octane/reference/variables/Subscribe.md create mode 100644 docs/reference/static-functions/functions/table_publishExternalState.md create mode 100644 examples/octane/aggregation/index.html create mode 100644 examples/octane/aggregation/package.json create mode 100644 examples/octane/aggregation/src/index.css create mode 100644 examples/octane/aggregation/src/main.tsrx create mode 100644 examples/octane/aggregation/src/makeData.ts create mode 100644 examples/octane/aggregation/src/vite-env.d.ts create mode 100644 examples/octane/aggregation/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/aggregation/tsconfig.json create mode 100644 examples/octane/aggregation/vite.config.ts create mode 100644 examples/octane/basic-dynamic-columns/index.html create mode 100644 examples/octane/basic-dynamic-columns/package.json create mode 100644 examples/octane/basic-dynamic-columns/src/index.css create mode 100644 examples/octane/basic-dynamic-columns/src/main.tsrx create mode 100644 examples/octane/basic-dynamic-columns/src/makeData.ts create mode 100644 examples/octane/basic-dynamic-columns/src/vite-env.d.ts create mode 100644 examples/octane/basic-dynamic-columns/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/basic-dynamic-columns/tsconfig.json create mode 100644 examples/octane/basic-dynamic-columns/vite.config.ts create mode 100644 examples/octane/basic-external-atoms/index.html create mode 100644 examples/octane/basic-external-atoms/package.json create mode 100644 examples/octane/basic-external-atoms/src/index.css create mode 100644 examples/octane/basic-external-atoms/src/main.tsrx create mode 100644 examples/octane/basic-external-atoms/src/makeData.ts create mode 100644 examples/octane/basic-external-atoms/src/vite-env.d.ts create mode 100644 examples/octane/basic-external-atoms/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/basic-external-atoms/tsconfig.json create mode 100644 examples/octane/basic-external-atoms/vite.config.ts create mode 100644 examples/octane/basic-external-state/index.html create mode 100644 examples/octane/basic-external-state/package.json create mode 100644 examples/octane/basic-external-state/src/index.css create mode 100644 examples/octane/basic-external-state/src/main.tsrx create mode 100644 examples/octane/basic-external-state/src/makeData.ts create mode 100644 examples/octane/basic-external-state/src/vite-env.d.ts create mode 100644 examples/octane/basic-external-state/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/basic-external-state/tsconfig.json create mode 100644 examples/octane/basic-external-state/vite.config.ts create mode 100644 examples/octane/basic-subscribe/index.html create mode 100644 examples/octane/basic-subscribe/package.json create mode 100644 examples/octane/basic-subscribe/src/index.css create mode 100644 examples/octane/basic-subscribe/src/main.tsrx create mode 100644 examples/octane/basic-subscribe/src/makeData.ts create mode 100644 examples/octane/basic-subscribe/src/vite-env.d.ts create mode 100644 examples/octane/basic-subscribe/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/basic-subscribe/tsconfig.json create mode 100644 examples/octane/basic-subscribe/vite.config.ts create mode 100644 examples/octane/basic-use-app-table/index.html create mode 100644 examples/octane/basic-use-app-table/package.json create mode 100644 examples/octane/basic-use-app-table/src/index.css create mode 100644 examples/octane/basic-use-app-table/src/main.tsrx create mode 100644 examples/octane/basic-use-app-table/src/vite-env.d.ts create mode 100644 examples/octane/basic-use-app-table/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/basic-use-app-table/tsconfig.json create mode 100644 examples/octane/basic-use-app-table/vite.config.ts create mode 100644 examples/octane/basic-use-table/index.html create mode 100644 examples/octane/basic-use-table/package.json create mode 100644 examples/octane/basic-use-table/src/index.css create mode 100644 examples/octane/basic-use-table/src/main.tsrx create mode 100644 examples/octane/basic-use-table/src/vite-env.d.ts create mode 100644 examples/octane/basic-use-table/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/basic-use-table/tsconfig.json create mode 100644 examples/octane/basic-use-table/vite.config.ts create mode 100644 examples/octane/column-ordering/index.html create mode 100644 examples/octane/column-ordering/package.json create mode 100644 examples/octane/column-ordering/src/index.css create mode 100644 examples/octane/column-ordering/src/main.tsrx create mode 100644 examples/octane/column-ordering/src/makeData.ts create mode 100644 examples/octane/column-ordering/src/vite-env.d.ts create mode 100644 examples/octane/column-ordering/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/column-ordering/tsconfig.json create mode 100644 examples/octane/column-ordering/vite.config.ts create mode 100644 examples/octane/column-pinning-split/index.html create mode 100644 examples/octane/column-pinning-split/package.json create mode 100644 examples/octane/column-pinning-split/src/index.css create mode 100644 examples/octane/column-pinning-split/src/main.tsrx create mode 100644 examples/octane/column-pinning-split/src/makeData.ts create mode 100644 examples/octane/column-pinning-split/src/vite-env.d.ts create mode 100644 examples/octane/column-pinning-split/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/column-pinning-split/tsconfig.json create mode 100644 examples/octane/column-pinning-split/vite.config.ts create mode 100644 examples/octane/column-pinning-sticky/index.html create mode 100644 examples/octane/column-pinning-sticky/package.json create mode 100644 examples/octane/column-pinning-sticky/src/index.css create mode 100644 examples/octane/column-pinning-sticky/src/main.tsrx create mode 100644 examples/octane/column-pinning-sticky/src/makeData.ts create mode 100644 examples/octane/column-pinning-sticky/src/vite-env.d.ts create mode 100644 examples/octane/column-pinning-sticky/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/column-pinning-sticky/tsconfig.json create mode 100644 examples/octane/column-pinning-sticky/vite.config.ts create mode 100644 examples/octane/column-pinning/index.html create mode 100644 examples/octane/column-pinning/package.json create mode 100644 examples/octane/column-pinning/src/index.css create mode 100644 examples/octane/column-pinning/src/main.tsrx create mode 100644 examples/octane/column-pinning/src/makeData.ts create mode 100644 examples/octane/column-pinning/src/vite-env.d.ts create mode 100644 examples/octane/column-pinning/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/column-pinning/tsconfig.json create mode 100644 examples/octane/column-pinning/vite.config.ts create mode 100644 examples/octane/column-resizing-performant/index.html create mode 100644 examples/octane/column-resizing-performant/package.json create mode 100644 examples/octane/column-resizing-performant/src/index.css create mode 100644 examples/octane/column-resizing-performant/src/main.tsrx create mode 100644 examples/octane/column-resizing-performant/src/makeData.ts create mode 100644 examples/octane/column-resizing-performant/src/vite-env.d.ts create mode 100644 examples/octane/column-resizing-performant/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/column-resizing-performant/tsconfig.json create mode 100644 examples/octane/column-resizing-performant/vite.config.ts create mode 100644 examples/octane/column-resizing/index.html create mode 100644 examples/octane/column-resizing/package.json create mode 100644 examples/octane/column-resizing/src/index.css create mode 100644 examples/octane/column-resizing/src/main.tsrx create mode 100644 examples/octane/column-resizing/src/makeData.ts create mode 100644 examples/octane/column-resizing/src/vite-env.d.ts create mode 100644 examples/octane/column-resizing/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/column-resizing/tsconfig.json create mode 100644 examples/octane/column-resizing/vite.config.ts create mode 100644 examples/octane/column-sizing/index.html create mode 100644 examples/octane/column-sizing/package.json create mode 100644 examples/octane/column-sizing/src/index.css create mode 100644 examples/octane/column-sizing/src/main.tsrx create mode 100644 examples/octane/column-sizing/src/makeData.ts create mode 100644 examples/octane/column-sizing/src/vite-env.d.ts create mode 100644 examples/octane/column-sizing/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/column-sizing/tsconfig.json create mode 100644 examples/octane/column-sizing/vite.config.ts create mode 100644 examples/octane/column-visibility/index.html create mode 100644 examples/octane/column-visibility/package.json create mode 100644 examples/octane/column-visibility/src/index.css create mode 100644 examples/octane/column-visibility/src/main.tsrx create mode 100644 examples/octane/column-visibility/src/makeData.ts create mode 100644 examples/octane/column-visibility/src/vite-env.d.ts create mode 100644 examples/octane/column-visibility/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/column-visibility/tsconfig.json create mode 100644 examples/octane/column-visibility/vite.config.ts create mode 100644 examples/octane/composable-tables/index.html create mode 100644 examples/octane/composable-tables/package.json create mode 100644 examples/octane/composable-tables/src/components/cell-components.tsrx create mode 100644 examples/octane/composable-tables/src/components/header-components.tsrx create mode 100644 examples/octane/composable-tables/src/components/indeterminate-checkbox.tsrx create mode 100644 examples/octane/composable-tables/src/components/table-components.tsrx create mode 100644 examples/octane/composable-tables/src/hooks/table.ts create mode 100644 examples/octane/composable-tables/src/index.css create mode 100644 examples/octane/composable-tables/src/main.tsrx create mode 100644 examples/octane/composable-tables/src/makeData.ts create mode 100644 examples/octane/composable-tables/src/vite-env.d.ts create mode 100644 examples/octane/composable-tables/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/composable-tables/tsconfig.json create mode 100644 examples/octane/composable-tables/vite.config.ts create mode 100644 examples/octane/custom-plugin/index.html create mode 100644 examples/octane/custom-plugin/package.json create mode 100644 examples/octane/custom-plugin/src/index.css create mode 100644 examples/octane/custom-plugin/src/main.tsrx create mode 100644 examples/octane/custom-plugin/src/makeData.ts create mode 100644 examples/octane/custom-plugin/src/vite-env.d.ts create mode 100644 examples/octane/custom-plugin/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/custom-plugin/tsconfig.json create mode 100644 examples/octane/custom-plugin/vite.config.ts create mode 100644 examples/octane/expanding/index.html create mode 100644 examples/octane/expanding/package.json create mode 100644 examples/octane/expanding/src/index.css create mode 100644 examples/octane/expanding/src/main.tsrx create mode 100644 examples/octane/expanding/src/makeData.ts create mode 100644 examples/octane/expanding/src/vite-env.d.ts create mode 100644 examples/octane/expanding/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/expanding/tsconfig.json create mode 100644 examples/octane/expanding/vite.config.ts create mode 100644 examples/octane/filters-faceted-bucketed/index.html create mode 100644 examples/octane/filters-faceted-bucketed/package.json create mode 100644 examples/octane/filters-faceted-bucketed/src/index.css create mode 100644 examples/octane/filters-faceted-bucketed/src/main.tsrx create mode 100644 examples/octane/filters-faceted-bucketed/src/makeData.ts create mode 100644 examples/octane/filters-faceted-bucketed/src/vite-env.d.ts create mode 100644 examples/octane/filters-faceted-bucketed/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/filters-faceted-bucketed/tsconfig.json create mode 100644 examples/octane/filters-faceted-bucketed/vite.config.ts create mode 100644 examples/octane/filters-faceted/index.html create mode 100644 examples/octane/filters-faceted/package.json create mode 100644 examples/octane/filters-faceted/src/index.css create mode 100644 examples/octane/filters-faceted/src/main.tsrx create mode 100644 examples/octane/filters-faceted/src/makeData.ts create mode 100644 examples/octane/filters-faceted/src/vite-env.d.ts create mode 100644 examples/octane/filters-faceted/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/filters-faceted/tsconfig.json create mode 100644 examples/octane/filters-faceted/vite.config.ts create mode 100644 examples/octane/filters-fuzzy/index.html create mode 100644 examples/octane/filters-fuzzy/package.json create mode 100644 examples/octane/filters-fuzzy/src/index.css create mode 100644 examples/octane/filters-fuzzy/src/main.tsrx create mode 100644 examples/octane/filters-fuzzy/src/makeData.ts create mode 100644 examples/octane/filters-fuzzy/src/vite-env.d.ts create mode 100644 examples/octane/filters-fuzzy/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/filters-fuzzy/tsconfig.json create mode 100644 examples/octane/filters-fuzzy/vite.config.ts create mode 100644 examples/octane/filters/index.html create mode 100644 examples/octane/filters/package.json create mode 100644 examples/octane/filters/src/index.css create mode 100644 examples/octane/filters/src/main.tsrx create mode 100644 examples/octane/filters/src/makeData.ts create mode 100644 examples/octane/filters/src/vite-env.d.ts create mode 100644 examples/octane/filters/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/filters/tsconfig.json create mode 100644 examples/octane/filters/vite.config.ts create mode 100644 examples/octane/grouped-aggregation/index.html create mode 100644 examples/octane/grouped-aggregation/package.json create mode 100644 examples/octane/grouped-aggregation/src/index.css create mode 100644 examples/octane/grouped-aggregation/src/main.tsrx create mode 100644 examples/octane/grouped-aggregation/src/makeData.ts create mode 100644 examples/octane/grouped-aggregation/src/vite-env.d.ts create mode 100644 examples/octane/grouped-aggregation/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/grouped-aggregation/tsconfig.json create mode 100644 examples/octane/grouped-aggregation/vite.config.ts create mode 100644 examples/octane/grouping/index.html create mode 100644 examples/octane/grouping/package.json create mode 100644 examples/octane/grouping/src/index.css create mode 100644 examples/octane/grouping/src/main.tsrx create mode 100644 examples/octane/grouping/src/makeData.ts create mode 100644 examples/octane/grouping/src/vite-env.d.ts create mode 100644 examples/octane/grouping/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/grouping/tsconfig.json create mode 100644 examples/octane/grouping/vite.config.ts create mode 100644 examples/octane/header-groups/index.html create mode 100644 examples/octane/header-groups/package.json create mode 100644 examples/octane/header-groups/src/index.css create mode 100644 examples/octane/header-groups/src/main.tsrx create mode 100644 examples/octane/header-groups/src/makeData.ts create mode 100644 examples/octane/header-groups/src/vite-env.d.ts create mode 100644 examples/octane/header-groups/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/header-groups/tsconfig.json create mode 100644 examples/octane/header-groups/vite.config.ts create mode 100644 examples/octane/kitchen-sink/index.html create mode 100644 examples/octane/kitchen-sink/package.json create mode 100644 examples/octane/kitchen-sink/src/index.css create mode 100644 examples/octane/kitchen-sink/src/main.tsrx create mode 100644 examples/octane/kitchen-sink/src/makeData.ts create mode 100644 examples/octane/kitchen-sink/src/vite-env.d.ts create mode 100644 examples/octane/kitchen-sink/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/kitchen-sink/tsconfig.json create mode 100644 examples/octane/kitchen-sink/vite.config.ts create mode 100644 examples/octane/pagination/index.html create mode 100644 examples/octane/pagination/package.json create mode 100644 examples/octane/pagination/src/index.css create mode 100644 examples/octane/pagination/src/main.tsrx create mode 100644 examples/octane/pagination/src/makeData.ts create mode 100644 examples/octane/pagination/src/vite-env.d.ts create mode 100644 examples/octane/pagination/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/pagination/tsconfig.json create mode 100644 examples/octane/pagination/vite.config.ts create mode 100644 examples/octane/row-pinning/index.html create mode 100644 examples/octane/row-pinning/package.json create mode 100644 examples/octane/row-pinning/src/index.css create mode 100644 examples/octane/row-pinning/src/main.tsrx create mode 100644 examples/octane/row-pinning/src/makeData.ts create mode 100644 examples/octane/row-pinning/src/vite-env.d.ts create mode 100644 examples/octane/row-pinning/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/row-pinning/tsconfig.json create mode 100644 examples/octane/row-pinning/vite.config.ts create mode 100644 examples/octane/row-selection/index.html create mode 100644 examples/octane/row-selection/package.json create mode 100644 examples/octane/row-selection/src/index.css create mode 100644 examples/octane/row-selection/src/main.tsrx create mode 100644 examples/octane/row-selection/src/makeData.ts create mode 100644 examples/octane/row-selection/src/vite-env.d.ts create mode 100644 examples/octane/row-selection/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/row-selection/tsconfig.json create mode 100644 examples/octane/row-selection/vite.config.ts create mode 100644 examples/octane/sorting/.gitignore create mode 100644 examples/octane/sorting/index.html create mode 100644 examples/octane/sorting/package.json create mode 100644 examples/octane/sorting/src/index.css create mode 100644 examples/octane/sorting/src/main.tsrx create mode 100644 examples/octane/sorting/src/makeData.ts create mode 100644 examples/octane/sorting/src/vite-env.d.ts create mode 100644 examples/octane/sorting/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/sorting/tsconfig.json create mode 100644 examples/octane/sorting/vite.config.ts create mode 100644 examples/octane/sub-components/index.html create mode 100644 examples/octane/sub-components/package.json create mode 100644 examples/octane/sub-components/src/index.css create mode 100644 examples/octane/sub-components/src/main.tsrx create mode 100644 examples/octane/sub-components/src/makeData.ts create mode 100644 examples/octane/sub-components/src/vite-env.d.ts create mode 100644 examples/octane/sub-components/tests/e2e/smoke.spec.ts create mode 100644 examples/octane/sub-components/tsconfig.json create mode 100644 examples/octane/sub-components/vite.config.ts create mode 100644 packages/octane-table/README.md create mode 100644 packages/octane-table/docs-entry.d.ts create mode 100644 packages/octane-table/package.json create mode 100644 packages/octane-table/src/FlexRender.ts create mode 100644 packages/octane-table/src/Subscribe.tsrx create mode 100644 packages/octane-table/src/Subscribe.tsrx.d.ts create mode 100644 packages/octane-table/src/createTableHook.tsrx create mode 100644 packages/octane-table/src/createTableHook.tsrx.d.ts create mode 100644 packages/octane-table/src/createTableHookContexts.ts create mode 100644 packages/octane-table/src/experimental-worker-plugin.ts create mode 100644 packages/octane-table/src/flex-render.ts create mode 100644 packages/octane-table/src/index.d.ts create mode 100644 packages/octane-table/src/index.ts create mode 100644 packages/octane-table/src/internal.ts create mode 100644 packages/octane-table/src/reactivity.ts create mode 100644 packages/octane-table/src/static-functions.ts create mode 100644 packages/octane-table/src/types.ts create mode 100644 packages/octane-table/src/useTable.tsrx create mode 100644 packages/octane-table/src/useTable.tsrx.d.ts create mode 100644 packages/octane-table/tests/_fixtures/adapter-reactivity.tsrx create mode 100644 packages/octane-table/tests/_fixtures/app-table.tsrx create mode 100644 packages/octane-table/tests/_fixtures/features.tsrx create mode 100644 packages/octane-table/tests/_fixtures/flex-render.tsrx create mode 100644 packages/octane-table/tests/_fixtures/ssr-table.tsrx create mode 100644 packages/octane-table/tests/_fixtures/table-basic.tsrx create mode 100644 packages/octane-table/tests/_helpers.ts create mode 100644 packages/octane-table/tests/conformance/adapter-reactivity.test.ts create mode 100644 packages/octane-table/tests/conformance/app-table.test.ts create mode 100644 packages/octane-table/tests/conformance/core.test.ts create mode 100644 packages/octane-table/tests/conformance/features.test.ts create mode 100644 packages/octane-table/tests/conformance/flex-render.test.ts create mode 100644 packages/octane-table/tests/conformance/parity.test.ts create mode 100644 packages/octane-table/tests/ssr/ssr.test.ts create mode 100644 packages/octane-table/tests/test-setup.ts create mode 100644 packages/octane-table/tests/type-tests/public-surface.tsx create mode 100644 packages/octane-table/tsconfig.docs.json create mode 100644 packages/octane-table/tsconfig.json create mode 100644 packages/octane-table/vite.config.ts create mode 100644 packages/octane-table/vitest.ssr.config.ts diff --git a/docs/config.json b/docs/config.json index 966d3ca75c..ae593d0f0b 100644 --- a/docs/config.json +++ b/docs/config.json @@ -56,6 +56,12 @@ { "label": "Migrating to V9", "to": "framework/preact/guide/migrating" } ] }, + { + "label": "octane", + "children": [ + { "label": "Quick Start", "to": "framework/octane/quick-start" } + ] + }, { "label": "solid", "children": [ @@ -162,6 +168,15 @@ { "label": "Custom Plugins", "to": "framework/preact/guide/custom-features" } ] }, + { + "label": "octane", + "children": [ + { "label": "Table State", "to": "framework/octane/guide/table-state" }, + { "label": "Composable Tables (createTableHook)", "to": "framework/octane/guide/composable-tables" }, + { "label": "Table Context", "to": "framework/octane/guide/table-context" }, + { "label": "Custom Plugins", "to": "framework/octane/guide/custom-features" } + ] + }, { "label": "solid", "children": [ @@ -334,6 +349,28 @@ { "label": "Virtualization", "to": "framework/preact/guide/virtualization" } ] }, + { + "label": "octane", + "children": [ + { "label": "Cell Selection", "to": "framework/octane/guide/cell-selection" }, + { "label": "Column Ordering", "to": "framework/octane/guide/column-ordering" }, + { "label": "Column Pinning", "to": "framework/octane/guide/column-pinning" }, + { "label": "Column Sizing", "to": "framework/octane/guide/column-sizing" }, + { "label": "Column Resizing", "to": "framework/octane/guide/column-resizing" }, + { "label": "Column Visibility", "to": "framework/octane/guide/column-visibility" }, + { "label": "Column Filtering", "to": "framework/octane/guide/column-filtering" }, + { "label": "Global Filtering", "to": "framework/octane/guide/global-filtering" }, + { "label": "Fuzzy Filtering", "to": "framework/octane/guide/fuzzy-filtering" }, + { "label": "Faceting", "to": "framework/octane/guide/column-faceting" }, + { "label": "Aggregation", "to": "framework/octane/guide/aggregation" }, + { "label": "Grouping", "to": "framework/octane/guide/grouping" }, + { "label": "Expanding", "to": "framework/octane/guide/expanding" }, + { "label": "Pagination", "to": "framework/octane/guide/pagination" }, + { "label": "Row Pinning", "to": "framework/octane/guide/row-pinning" }, + { "label": "Row Selection", "to": "framework/octane/guide/row-selection" }, + { "label": "Sorting", "to": "framework/octane/guide/sorting" } + ] + }, { "label": "solid", "children": [ @@ -447,6 +484,12 @@ { "label": "Preact API Reference", "to": "framework/preact/reference/index" } ] }, + { + "label": "octane", + "children": [ + { "label": "Octane API Reference", "to": "framework/octane/reference/index" } + ] + }, { "label": "solid", "children": [ @@ -570,6 +613,20 @@ { "label": "flexRender", "to": "framework/preact/reference/functions/flexRender" } ] }, + { + "label": "octane", + "children": [ + { "label": "useTable", "to": "framework/octane/reference/functions/useTable" }, + { "label": "createTableHook", "to": "framework/octane/reference/functions/createTableHook" }, + { "label": "createTableHookContexts", "to": "framework/octane/reference/functions/createTableHookContexts" }, + { "label": "OctaneTable", "to": "framework/octane/reference/type-aliases/OctaneTable" }, + { "label": "AppOctaneTable", "to": "framework/octane/reference/type-aliases/AppOctaneTable" }, + { "label": "CreateTableHookOptions", "to": "framework/octane/reference/type-aliases/CreateTableHookOptions" }, + { "label": "Subscribe", "to": "framework/octane/reference/variables/Subscribe" }, + { "label": "FlexRender", "to": "framework/octane/reference/functions/FlexRender-1" }, + { "label": "flexRender", "to": "framework/octane/reference/functions/flexRender" } + ] + }, { "label": "solid", "children": [ @@ -675,6 +732,12 @@ { "label": "AppColumnHelper", "to": "framework/preact/reference/type-aliases/AppColumnHelper" } ] }, + { + "label": "octane", + "children": [ + { "label": "AppColumnHelper", "to": "framework/octane/reference/interfaces/AppColumnHelper" } + ] + }, { "label": "solid", "children": [ @@ -754,6 +817,12 @@ { "label": "AppCellContext", "to": "framework/preact/reference/type-aliases/AppCellContext" } ] }, + { + "label": "octane", + "children": [ + { "label": "AppCellContext", "to": "framework/octane/reference/interfaces/AppCellContext" } + ] + }, { "label": "solid", "children": [ @@ -810,6 +879,12 @@ { "label": "AppHeaderContext", "to": "framework/preact/reference/type-aliases/AppHeaderContext" } ] }, + { + "label": "octane", + "children": [ + { "label": "AppHeaderContext", "to": "framework/octane/reference/interfaces/AppHeaderContext" } + ] + }, { "label": "solid", "children": [ @@ -1053,6 +1128,18 @@ { "label": "Header Groups", "to": "framework/preact/examples/header-groups" } ] }, + { + "label": "octane", + "children": [ + { "label": "Basic (useTable)", "to": "framework/octane/examples/basic-use-table" }, + { "label": "Basic (useAppTable)", "to": "framework/octane/examples/basic-use-app-table" }, + { "label": "Basic (External State)", "to": "framework/octane/examples/basic-external-state" }, + { "label": "Basic (External Atoms)", "to": "framework/octane/examples/basic-external-atoms" }, + { "label": "Basic (Subscribe)", "to": "framework/octane/examples/basic-subscribe" }, + { "label": "Basic (Dynamic Columns)", "to": "framework/octane/examples/basic-dynamic-columns" }, + { "label": "Header Groups", "to": "framework/octane/examples/header-groups" } + ] + }, { "label": "vanilla", "children": [ @@ -1319,6 +1406,33 @@ { "label": "Sorting", "to": "framework/preact/examples/sorting" } ] }, + { + "label": "octane", + "children": [ + { "label": "Kitchen Sink (All Features)", "to": "framework/octane/examples/kitchen-sink" }, + { "label": "Column Filters", "to": "framework/octane/examples/filters" }, + { "label": "Column Filters (Faceted)", "to": "framework/octane/examples/filters-faceted" }, + { "label": "Bucketed Faceted Filters", "to": "framework/octane/examples/filters-faceted-bucketed" }, + { "label": "Fuzzy Search Filters", "to": "framework/octane/examples/filters-fuzzy" }, + { "label": "Column Ordering", "to": "framework/octane/examples/column-ordering" }, + { "label": "Column Pinning", "to": "framework/octane/examples/column-pinning" }, + { "label": "Column Pinning (Split)", "to": "framework/octane/examples/column-pinning-split" }, + { "label": "Sticky Column Pinning", "to": "framework/octane/examples/column-pinning-sticky" }, + { "label": "Column Sizing", "to": "framework/octane/examples/column-sizing" }, + { "label": "Column Resizing", "to": "framework/octane/examples/column-resizing" }, + { "label": "Performant Column Resizing", "to": "framework/octane/examples/column-resizing-performant" }, + { "label": "Column Visibility", "to": "framework/octane/examples/column-visibility" }, + { "label": "Expanding", "to": "framework/octane/examples/expanding" }, + { "label": "Expanding Sub Components", "to": "framework/octane/examples/sub-components" }, + { "label": "Grouping", "to": "framework/octane/examples/grouping" }, + { "label": "Aggregation", "to": "framework/octane/examples/aggregation" }, + { "label": "Grouped Aggregation", "to": "framework/octane/examples/grouped-aggregation" }, + { "label": "Pagination", "to": "framework/octane/examples/pagination" }, + { "label": "Row Pinning", "to": "framework/octane/examples/row-pinning" }, + { "label": "Row Selection", "to": "framework/octane/examples/row-selection" }, + { "label": "Sorting", "to": "framework/octane/examples/sorting" } + ] + }, { "label": "vanilla", "children": [ @@ -1432,6 +1546,13 @@ { "label": "Custom Plugin", "to": "framework/preact/examples/custom-plugin" }, { "label": "With TanStack Query", "to": "framework/preact/examples/with-tanstack-query" } ] + }, + { + "label": "octane", + "children": [ + { "label": "Composable Tables (createTableHook)", "to": "framework/octane/examples/composable-tables" }, + { "label": "Custom Plugin", "to": "framework/octane/examples/custom-plugin" } + ] } ] }, diff --git a/docs/devtools.md b/docs/devtools.md index af2a63efa8..a2728129a5 100644 --- a/docs/devtools.md +++ b/docs/devtools.md @@ -31,6 +31,10 @@ npm install @tanstack/react-devtools @tanstack/react-table-devtools@beta npm install @tanstack/preact-devtools @tanstack/preact-table-devtools@beta ``` +# Octane + +There is not currently a dedicated Octane Table Devtools adapter. + # Vue ```sh @@ -51,7 +55,7 @@ npm install @tanstack/angular-devtools @tanstack/angular-table-devtools@beta -Lit, Svelte, Alpine, and vanilla do not currently ship dedicated table devtools adapters. +Octane, Lit, Svelte, Alpine, and vanilla do not currently ship dedicated table devtools adapters. ## The Required `key` Table Option @@ -146,6 +150,10 @@ render( ) ``` +# Octane + +Octane tables expose the same `table.state`, slice atoms, `table.store`, and `table.Subscribe` inspection surfaces, but there is not currently an Octane Table Devtools plugin to register. + See the [Preact row-selection example](./framework/preact/examples/row-selection). # Vue @@ -297,6 +305,10 @@ useTanStackTableDevtools(table, { enabled: false }) useTanStackTableDevtools(table, { enabled: false }) ``` +# Octane + +No adapter toggle is required because Octane does not currently ship a Table Devtools adapter. + # Vue ```ts @@ -344,6 +356,10 @@ import { } from '@tanstack/preact-table-devtools/production' ``` +# Octane + +There is no production Devtools entry point for Octane Table at this time. + # Vue ```tsx diff --git a/docs/framework/alpine/reference/type-aliases/FlexRenderProps.md b/docs/framework/alpine/reference/type-aliases/FlexRenderProps.md index 8331f6ea7c..68970001dd 100644 --- a/docs/framework/alpine/reference/type-aliases/FlexRenderProps.md +++ b/docs/framework/alpine/reference/type-aliases/FlexRenderProps.md @@ -6,7 +6,7 @@ title: FlexRenderProps # Type Alias: FlexRenderProps\ ```ts -type FlexRenderProps = +type FlexRenderProps = | { cell: Cell; footer?: never; diff --git a/docs/framework/angular/reference/classes/FlexRenderComponentInstance.md b/docs/framework/angular/reference/classes/FlexRenderComponentInstance.md index 84e331f523..52c97a24cb 100644 --- a/docs/framework/angular/reference/classes/FlexRenderComponentInstance.md +++ b/docs/framework/angular/reference/classes/FlexRenderComponentInstance.md @@ -27,11 +27,11 @@ Prefer [flexRenderComponent](../functions/flexRenderComponent.md) helper for bet ```ts new FlexRenderComponentInstance( - component, - inputs?, - injector?, - outputs?, - directives?, + component, + inputs?, + injector?, + outputs?, + directives?, bindings?): FlexRenderComponentInstance; ``` diff --git a/docs/framework/angular/reference/classes/FlexRenderDirective.md b/docs/framework/angular/reference/classes/FlexRenderDirective.md index ae53f2ee19..caa1d8d204 100644 --- a/docs/framework/angular/reference/classes/FlexRenderDirective.md +++ b/docs/framework/angular/reference/classes/FlexRenderDirective.md @@ -71,7 +71,7 @@ which the latter is preferred. ### TProps -`TProps` *extends* +`TProps` *extends* \| `NonNullable`\<`unknown`\> \| `CellContext`\<`TFeatures`, `TRowData`, `TValue`\> \| `HeaderContext`\<`TFeatures`, `TRowData`, `TValue`\> diff --git a/docs/framework/angular/reference/type-aliases/FlexRenderContent.md b/docs/framework/angular/reference/type-aliases/FlexRenderContent.md index 8b802c76fa..85cb24ace4 100644 --- a/docs/framework/angular/reference/type-aliases/FlexRenderContent.md +++ b/docs/framework/angular/reference/type-aliases/FlexRenderContent.md @@ -6,7 +6,7 @@ title: FlexRenderContent # Type Alias: FlexRenderContent\ ```ts -type FlexRenderContent = +type FlexRenderContent = | string | number | Type diff --git a/docs/framework/angular/reference/type-aliases/FlexRenderInputContent.md b/docs/framework/angular/reference/type-aliases/FlexRenderInputContent.md index 0d522c77c7..a3d1862dcd 100644 --- a/docs/framework/angular/reference/type-aliases/FlexRenderInputContent.md +++ b/docs/framework/angular/reference/type-aliases/FlexRenderInputContent.md @@ -6,7 +6,7 @@ title: FlexRenderInputContent # Type Alias: FlexRenderInputContent\ ```ts -type FlexRenderInputContent = +type FlexRenderInputContent = | number | string | (props) => FlexRenderContent diff --git a/docs/framework/angular/reference/type-aliases/RenderableComponent.md b/docs/framework/angular/reference/type-aliases/RenderableComponent.md index 110c4c558a..b194166761 100644 --- a/docs/framework/angular/reference/type-aliases/RenderableComponent.md +++ b/docs/framework/angular/reference/type-aliases/RenderableComponent.md @@ -6,7 +6,7 @@ title: RenderableComponent # Type Alias: RenderableComponent ```ts -type RenderableComponent = +type RenderableComponent = | Type | (props) => FlexRenderContent; ``` diff --git a/docs/framework/angular/reference/type-aliases/SubscribeSource.md b/docs/framework/angular/reference/type-aliases/SubscribeSource.md index cf222f7fda..531cddf8d5 100644 --- a/docs/framework/angular/reference/type-aliases/SubscribeSource.md +++ b/docs/framework/angular/reference/type-aliases/SubscribeSource.md @@ -6,7 +6,7 @@ title: SubscribeSource # Type Alias: SubscribeSource\ ```ts -type SubscribeSource = +type SubscribeSource = | Atom | ReadonlyAtom | Store diff --git a/docs/framework/ember/reference/classes/FlexRenderCell.md b/docs/framework/ember/reference/classes/FlexRenderCell.md index 8ea5d75ab8..7b0120de50 100644 --- a/docs/framework/ember/reference/classes/FlexRenderCell.md +++ b/docs/framework/ember/reference/classes/FlexRenderCell.md @@ -165,7 +165,7 @@ Defined in: packages/ember-table/declarations/FlexRender.d.ts:31 #### Get Signature ```ts -get componentToRender(): +get componentToRender(): | ComponentLike> | undefined; ``` diff --git a/docs/framework/ember/reference/classes/FlexRenderFooter.md b/docs/framework/ember/reference/classes/FlexRenderFooter.md index 6805ae8dd5..2fbad3417f 100644 --- a/docs/framework/ember/reference/classes/FlexRenderFooter.md +++ b/docs/framework/ember/reference/classes/FlexRenderFooter.md @@ -165,7 +165,7 @@ Defined in: packages/ember-table/declarations/FlexRender.d.ts:59 #### Get Signature ```ts -get componentToRender(): +get componentToRender(): | ComponentLike> | undefined; ``` diff --git a/docs/framework/ember/reference/classes/FlexRenderHeader.md b/docs/framework/ember/reference/classes/FlexRenderHeader.md index e829147d5e..b485fdeb26 100644 --- a/docs/framework/ember/reference/classes/FlexRenderHeader.md +++ b/docs/framework/ember/reference/classes/FlexRenderHeader.md @@ -165,7 +165,7 @@ Defined in: packages/ember-table/declarations/FlexRender.d.ts:45 #### Get Signature ```ts -get componentToRender(): +get componentToRender(): | ComponentLike> | undefined; ``` diff --git a/docs/framework/ember/reference/functions/createTableHook.md b/docs/framework/ember/reference/functions/createTableHook.md index baf000d03c..2340dacd14 100644 --- a/docs/framework/ember/reference/functions/createTableHook.md +++ b/docs/framework/ember/reference/functions/createTableHook.md @@ -60,22 +60,57 @@ createAppColumnHelper: () => ColumnHelper; ### createAppTable() ```ts -createAppTable: (getTableOptions) => AppEmberTable; +createAppTable: { + (owner, getTableOptions): AppEmberTable; + (getTableOptions): AppEmberTable; +}; ``` -#### Type Parameters +#### Call Signature -##### TData +```ts +(owner, getTableOptions): AppEmberTable; +``` + +##### Type Parameters + +###### TData `TData` *extends* `RowData` -#### Parameters +##### Parameters -##### getTableOptions +###### owner + +`object` + +###### getTableOptions () => `Omit`\<`TableOptions`\<`TFeatures`, `TData`\>, `"features"`\> -#### Returns +##### Returns + +[`AppEmberTable`](../type-aliases/AppEmberTable.md)\<`TFeatures`, `TData`\> + +#### Call Signature + +```ts +(getTableOptions): AppEmberTable; +``` + +##### Type Parameters + +###### TData + +`TData` *extends* `RowData` + +##### Parameters + +###### getTableOptions + +() => `Omit`\<`TableOptions`\<`TFeatures`, `TData`\>, `"features"`\> + +##### Returns [`AppEmberTable`](../type-aliases/AppEmberTable.md)\<`TFeatures`, `TData`\> @@ -89,6 +124,6 @@ const { createAppTable, createAppColumnHelper } = createTableHook({ const columnHelper = createAppColumnHelper() const columns = columnHelper.columns([...]) -// inside a Glimmer component; options stay a thunk so tracked reads are reactive -table = createAppTable(() => ({ columns, data: this.data })) +// inside a Glimmer component; passing `this` binds cleanup to its lifecycle +table = createAppTable(this, () => ({ columns, data: this.data })) ``` diff --git a/docs/framework/ember/reference/functions/useTable.md b/docs/framework/ember/reference/functions/useTable.md index c830fcc13d..cba642ed31 100644 --- a/docs/framework/ember/reference/functions/useTable.md +++ b/docs/framework/ember/reference/functions/useTable.md @@ -5,28 +5,76 @@ title: useTable # Function: useTable() +## Call Signature + +```ts +function useTable(owner, getOptions): Table; +``` + +Defined in: packages/ember-table/declarations/use-table.d.ts:10 + +Creates an Ember-reactive table. + +Pass the containing component (or another Ember destroyable) as the first +argument to tie external-atom subscriptions to its lifecycle. The one-arg +form remains available for standalone tables that do not have an Ember +owner. + +### Type Parameters + +#### TFeatures + +`TFeatures` *extends* `TableFeatures` + +#### TData + +`TData` *extends* `RowData` + +### Parameters + +#### owner + +`object` + +#### getOptions + +() => `TableOptions`\<`TFeatures`, `TData`\> + +### Returns + +`Table`\<`TFeatures`, `TData`\> + +## Call Signature + ```ts function useTable(getOptions): Table; ``` -Defined in: packages/ember-table/declarations/use-table.d.ts:2 +Defined in: packages/ember-table/declarations/use-table.d.ts:11 + +Creates an Ember-reactive table. + +Pass the containing component (or another Ember destroyable) as the first +argument to tie external-atom subscriptions to its lifecycle. The one-arg +form remains available for standalone tables that do not have an Ember +owner. -## Type Parameters +### Type Parameters -### TFeatures +#### TFeatures `TFeatures` *extends* `TableFeatures` -### TData +#### TData `TData` *extends* `RowData` -## Parameters +### Parameters -### getOptions +#### getOptions () => `TableOptions`\<`TFeatures`, `TData`\> -## Returns +### Returns `Table`\<`TFeatures`, `TData`\> diff --git a/docs/framework/ember/reference/type-aliases/FlexRenderContext.md b/docs/framework/ember/reference/type-aliases/FlexRenderContext.md index eb30b9ac97..80ad6edf1c 100644 --- a/docs/framework/ember/reference/type-aliases/FlexRenderContext.md +++ b/docs/framework/ember/reference/type-aliases/FlexRenderContext.md @@ -6,7 +6,7 @@ title: FlexRenderContext # Type Alias: FlexRenderContext\ ```ts -type FlexRenderContext = +type FlexRenderContext = | CellContext | HeaderContext; ``` diff --git a/docs/framework/lit/reference/classes/SubscribeDirective.md b/docs/framework/lit/reference/classes/SubscribeDirective.md index cba28097d1..210a2c716a 100644 --- a/docs/framework/lit/reference/classes/SubscribeDirective.md +++ b/docs/framework/lit/reference/classes/SubscribeDirective.md @@ -88,8 +88,8 @@ AsyncDirective._$isConnected ```ts _$initialize( - part, - parent, + part, + parent, attributeIndex): void; ``` @@ -129,7 +129,7 @@ AsyncDirective._$initialize disconnected(): void; ``` -Defined in: [packages/lit-table/src/subscribe-directive.ts:141](https://github.com/TanStack/table/blob/main/packages/lit-table/src/subscribe-directive.ts#L141) +Defined in: [packages/lit-table/src/subscribe-directive.ts:150](https://github.com/TanStack/table/blob/main/packages/lit-table/src/subscribe-directive.ts#L150) Cleans up the controller subscription when the directive is removed from the DOM. @@ -151,7 +151,7 @@ AsyncDirective.disconnected reconnected(): void; ``` -Defined in: [packages/lit-table/src/subscribe-directive.ts:146](https://github.com/TanStack/table/blob/main/packages/lit-table/src/subscribe-directive.ts#L146) +Defined in: [packages/lit-table/src/subscribe-directive.ts:155](https://github.com/TanStack/table/blob/main/packages/lit-table/src/subscribe-directive.ts#L155) Restores the controller subscription when the directive is re-attached to the DOM. @@ -213,8 +213,8 @@ AsyncDirective.render ```ts render( - source, - selector, + source, + selector, template): unknown; ``` diff --git a/docs/framework/lit/reference/classes/TableController.md b/docs/framework/lit/reference/classes/TableController.md index 42c62b305e..8bb19d3d41 100644 --- a/docs/framework/lit/reference/classes/TableController.md +++ b/docs/framework/lit/reference/classes/TableController.md @@ -5,7 +5,7 @@ title: TableController # Class: TableController\ -Defined in: [packages/lit-table/src/TableController.ts:118](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L118) +Defined in: [packages/lit-table/src/TableController.ts:123](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L123) A Lit ReactiveController for TanStack Table integration. @@ -56,7 +56,7 @@ class MyTable extends LitElement { new TableController(host): TableController; ``` -Defined in: [packages/lit-table/src/TableController.ts:132](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L132) +Defined in: [packages/lit-table/src/TableController.ts:144](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L144) #### Parameters @@ -76,7 +76,7 @@ Defined in: [packages/lit-table/src/TableController.ts:132](https://github.com/T host: ReactiveControllerHost; ``` -Defined in: [packages/lit-table/src/TableController.ts:122](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L122) +Defined in: [packages/lit-table/src/TableController.ts:127](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L127) ## Methods @@ -86,7 +86,7 @@ Defined in: [packages/lit-table/src/TableController.ts:122](https://github.com/T hostConnected(): void; ``` -Defined in: [packages/lit-table/src/TableController.ts:238](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L238) +Defined in: [packages/lit-table/src/TableController.ts:251](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L251) Called when the host is connected to the component tree. For custom element hosts, this corresponds to the `connectedCallback()` lifecycle, @@ -110,7 +110,7 @@ ReactiveController.hostConnected hostDisconnected(): void; ``` -Defined in: [packages/lit-table/src/TableController.ts:242](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L242) +Defined in: [packages/lit-table/src/TableController.ts:270](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L270) Called when the host is disconnected from the component tree. For custom element hosts, this corresponds to the `disconnectedCallback()` lifecycle, @@ -129,13 +129,36 @@ ReactiveController.hostDisconnected *** +### hostUpdated() + +```ts +hostUpdated(): void; +``` + +Defined in: [packages/lit-table/src/TableController.ts:258](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L258) + +Called after a host update, just before the host calls firstUpdated and +updated. It is not called in server-side rendering. + +#### Returns + +`void` + +#### Implementation of + +```ts +ReactiveController.hostUpdated +``` + +*** + ### table() ```ts table(tableOptions, selector?): LitTable; ``` -Defined in: [packages/lit-table/src/TableController.ts:152](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L152) +Defined in: [packages/lit-table/src/TableController.ts:164](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L164) Returns the Lit-backed table instance for the current render pass. diff --git a/docs/framework/lit/reference/functions/FlexRender-1.md b/docs/framework/lit/reference/functions/FlexRender-1.md index 26fe947fd8..c061ec9d2d 100644 --- a/docs/framework/lit/reference/functions/FlexRender-1.md +++ b/docs/framework/lit/reference/functions/FlexRender-1.md @@ -6,10 +6,10 @@ title: FlexRender # Function: FlexRender() ```ts -function FlexRender(props): string | TemplateResult | null; +function FlexRender(props): LitRenderable; ``` -Defined in: [packages/lit-table/src/flexRender.ts:90](https://github.com/TanStack/table/blob/main/packages/lit-table/src/flexRender.ts#L90) +Defined in: [packages/lit-table/src/flexRender.ts:101](https://github.com/TanStack/table/blob/main/packages/lit-table/src/flexRender.ts#L101) Simplified component wrapper of `flexRender`. Use this utility function to render headers, cells, or footers with custom markup. Only one prop (`cell`, `header`, or `footer`) may be passed. @@ -36,7 +36,7 @@ Only one prop (`cell`, `header`, or `footer`) may be passed. ## Returns -`string` \| `TemplateResult` \| `null` +[`LitRenderable`](../type-aliases/LitRenderable.md) ## Example diff --git a/docs/framework/lit/reference/functions/createTableHook.md b/docs/framework/lit/reference/functions/createTableHook.md index 6167c70940..3272de93ba 100644 --- a/docs/framework/lit/reference/functions/createTableHook.md +++ b/docs/framework/lit/reference/functions/createTableHook.md @@ -9,7 +9,7 @@ title: createTableHook function createTableHook(__namedParameters): CreateTableHookResult; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:503](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L503) +Defined in: [packages/lit-table/src/createTableHook.ts:504](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L504) Creates a custom table hook with pre-bound components for composition. diff --git a/docs/framework/lit/reference/functions/flexRender.md b/docs/framework/lit/reference/functions/flexRender.md index cf133cdc28..883d93927f 100644 --- a/docs/framework/lit/reference/functions/flexRender.md +++ b/docs/framework/lit/reference/functions/flexRender.md @@ -6,10 +6,10 @@ title: flexRender # Function: flexRender() ```ts -function flexRender(Comp, props): string | TemplateResult | null; +function flexRender(Comp, props): LitRenderable; ``` -Defined in: [packages/lit-table/src/flexRender.ts:22](https://github.com/TanStack/table/blob/main/packages/lit-table/src/flexRender.ts#L22) +Defined in: [packages/lit-table/src/flexRender.ts:37](https://github.com/TanStack/table/blob/main/packages/lit-table/src/flexRender.ts#L37) Renders a Lit table template value with the provided context props. @@ -27,7 +27,7 @@ convenience wrapper for table cell/header/footer objects. ### Comp -`string` | `TemplateResult` | (`props`) => `string` \| `TemplateResult` | `undefined` +[`LitRenderable`](../type-aliases/LitRenderable.md) | (`props`) => [`LitRenderable`](../type-aliases/LitRenderable.md) ### props @@ -35,7 +35,7 @@ convenience wrapper for table cell/header/footer objects. ## Returns -`string` \| `TemplateResult` \| `null` +[`LitRenderable`](../type-aliases/LitRenderable.md) ## Example diff --git a/docs/framework/lit/reference/index.md b/docs/framework/lit/reference/index.md index e0cabbd6ce..5517c61193 100644 --- a/docs/framework/lit/reference/index.md +++ b/docs/framework/lit/reference/index.md @@ -28,6 +28,7 @@ title: "@tanstack/lit-table" - [ComponentType](type-aliases/ComponentType.md) - [CreateTableHookOptions](type-aliases/CreateTableHookOptions.md) - [FlexRenderProps](type-aliases/FlexRenderProps.md) +- [LitRenderable](type-aliases/LitRenderable.md) - [LitTable](type-aliases/LitTable.md) - [SelectionSource](type-aliases/SelectionSource.md) diff --git a/docs/framework/lit/reference/interfaces/CreateTableHookResult.md b/docs/framework/lit/reference/interfaces/CreateTableHookResult.md index 2c6bc65772..4aea227a2e 100644 --- a/docs/framework/lit/reference/interfaces/CreateTableHookResult.md +++ b/docs/framework/lit/reference/interfaces/CreateTableHookResult.md @@ -5,7 +5,7 @@ title: CreateTableHookResult # Interface: CreateTableHookResult\ -Defined in: [packages/lit-table/src/createTableHook.ts:358](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L358) +Defined in: [packages/lit-table/src/createTableHook.ts:359](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L359) ## Type Parameters @@ -33,7 +33,7 @@ Defined in: [packages/lit-table/src/createTableHook.ts:358](https://github.com/T appFeatures: TFeatures; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:365](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L365) +Defined in: [packages/lit-table/src/createTableHook.ts:366](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L366) The features object that was passed to `createTableHook`. @@ -45,7 +45,7 @@ The features object that was passed to `createTableHook`. createAppColumnHelper: () => AppColumnHelper; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:370](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L370) +Defined in: [packages/lit-table/src/createTableHook.ts:371](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L371) A column helper pre-bound to `TFeatures` and the registered components, so the cell/header/footer render props expose the bound components. @@ -68,7 +68,7 @@ the cell/header/footer render props expose the bound components. useAppTable: (host, tableOptions, selector?) => object; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:381](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L381) +Defined in: [packages/lit-table/src/createTableHook.ts:382](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L382) Creates a controller-like object whose `table()` method returns a table with the `App*` wrapper functions, a bound `FlexRender`, and the registered @@ -120,7 +120,7 @@ table: () => AppLitTable(host) => ContextConsumer>, ReactiveControllerHost & HTMLElement>; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:410](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L410) +Defined in: [packages/lit-table/src/createTableHook.ts:411](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L411) Reads the cell instance from a `@lit/context` `ContextConsumer`. lit never provides an extended cell through context, so this is a BARE `Cell`. @@ -149,7 +149,7 @@ provides an extended cell through context, so this is a BARE `Cell`. useHeaderContext: (host) => ContextConsumer>, ReactiveControllerHost & HTMLElement>; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:420](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L420) +Defined in: [packages/lit-table/src/createTableHook.ts:421](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L421) Reads the header instance from a `@lit/context` `ContextConsumer`. lit never provides an extended header through context, so this is a BARE `Header`. @@ -178,7 +178,7 @@ provides an extended header through context, so this is a BARE `Header`. useTableContext: (host) => ContextConsumer>, ReactiveControllerHost & HTMLElement>; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:400](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L400) +Defined in: [packages/lit-table/src/createTableHook.ts:401](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L401) Reads the table provided by the nearest ancestor that called `useAppTable`, via a `@lit/context` `ContextConsumer`. This is the BARE `LitTable` written diff --git a/docs/framework/lit/reference/type-aliases/AppCellContext.md b/docs/framework/lit/reference/type-aliases/AppCellContext.md index 7e013ef7fb..05e996c13c 100644 --- a/docs/framework/lit/reference/type-aliases/AppCellContext.md +++ b/docs/framework/lit/reference/type-aliases/AppCellContext.md @@ -9,7 +9,7 @@ title: AppCellContext type AppCellContext = object; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:48](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L48) +Defined in: [packages/lit-table/src/createTableHook.ts:49](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L49) Enhanced CellContext with pre-bound cell components. The `cell` property includes the registered cellComponents. @@ -40,19 +40,19 @@ The `cell` property includes the registered cellComponents. cell: Cell & BoundComponents & object; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:54](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L54) +Defined in: [packages/lit-table/src/createTableHook.ts:55](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L55) #### Type Declaration ##### FlexRender() ```ts -FlexRender: () => TemplateResult | string | null; +FlexRender: () => LitRenderable; ``` ###### Returns -`TemplateResult` \| `string` \| `null` +[`LitRenderable`](LitRenderable.md) *** @@ -62,7 +62,7 @@ FlexRender: () => TemplateResult | string | null; column: Column; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:58](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L58) +Defined in: [packages/lit-table/src/createTableHook.ts:59](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L59) *** @@ -72,7 +72,7 @@ Defined in: [packages/lit-table/src/createTableHook.ts:58](https://github.com/Ta getValue: CellContext["getValue"]; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:59](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L59) +Defined in: [packages/lit-table/src/createTableHook.ts:60](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L60) *** @@ -82,7 +82,7 @@ Defined in: [packages/lit-table/src/createTableHook.ts:59](https://github.com/Ta renderValue: CellContext["renderValue"]; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:60](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L60) +Defined in: [packages/lit-table/src/createTableHook.ts:61](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L61) *** @@ -92,7 +92,7 @@ Defined in: [packages/lit-table/src/createTableHook.ts:60](https://github.com/Ta row: Row; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:61](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L61) +Defined in: [packages/lit-table/src/createTableHook.ts:62](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L62) *** @@ -102,4 +102,4 @@ Defined in: [packages/lit-table/src/createTableHook.ts:61](https://github.com/Ta table: Table; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:62](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L62) +Defined in: [packages/lit-table/src/createTableHook.ts:63](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L63) diff --git a/docs/framework/lit/reference/type-aliases/AppColumnDefBase.md b/docs/framework/lit/reference/type-aliases/AppColumnDefBase.md index bfc76c591c..44646f83b6 100644 --- a/docs/framework/lit/reference/type-aliases/AppColumnDefBase.md +++ b/docs/framework/lit/reference/type-aliases/AppColumnDefBase.md @@ -9,7 +9,7 @@ title: AppColumnDefBase type AppColumnDefBase = Omit, "cell" | "header" | "footer"> & object; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:97](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L97) +Defined in: [packages/lit-table/src/createTableHook.ts:98](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L98) Enhanced column definition base with pre-bound components in cell/header/footer contexts. diff --git a/docs/framework/lit/reference/type-aliases/AppColumnDefTemplate.md b/docs/framework/lit/reference/type-aliases/AppColumnDefTemplate.md index 10e2a5e356..94ea5c6c65 100644 --- a/docs/framework/lit/reference/type-aliases/AppColumnDefTemplate.md +++ b/docs/framework/lit/reference/type-aliases/AppColumnDefTemplate.md @@ -9,7 +9,7 @@ title: AppColumnDefTemplate type AppColumnDefTemplate = string | (props) => any; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:90](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L90) +Defined in: [packages/lit-table/src/createTableHook.ts:91](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L91) Template type for column definitions that can be a string or a function. diff --git a/docs/framework/lit/reference/type-aliases/AppColumnHelper.md b/docs/framework/lit/reference/type-aliases/AppColumnHelper.md index 267fff63b1..352dc3f684 100644 --- a/docs/framework/lit/reference/type-aliases/AppColumnHelper.md +++ b/docs/framework/lit/reference/type-aliases/AppColumnHelper.md @@ -9,7 +9,7 @@ title: AppColumnHelper type AppColumnHelper = object; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:173](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L173) +Defined in: [packages/lit-table/src/createTableHook.ts:174](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L174) Enhanced column helper with pre-bound components in cell/header/footer contexts. This enables TypeScript to know about the registered components when defining columns. @@ -40,7 +40,7 @@ This enables TypeScript to know about the registered components when defining co accessor: (accessor, column) => TAccessor extends AccessorFn ? AccessorFnColumnDef : AccessorKeyColumnDef; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:183](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L183) +Defined in: [packages/lit-table/src/createTableHook.ts:184](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L184) Creates a data column definition with an accessor key or function. The cell, header, and footer contexts include pre-bound components. @@ -77,7 +77,7 @@ The cell, header, and footer contexts include pre-bound components. columns: (columns) => ColumnDef[] & [...TColumns]; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:214](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L214) +Defined in: [packages/lit-table/src/createTableHook.ts:215](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L215) Wraps an array of column definitions to preserve each column's individual TValue type. @@ -105,7 +105,7 @@ Wraps an array of column definitions to preserve each column's individual TValue display: (column) => DisplayColumnDef; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:222](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L222) +Defined in: [packages/lit-table/src/createTableHook.ts:223](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L223) Creates a display column definition for non-data columns. The cell, header, and footer contexts include pre-bound components. @@ -128,7 +128,7 @@ The cell, header, and footer contexts include pre-bound components. group: (column) => GroupColumnDef; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:235](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L235) +Defined in: [packages/lit-table/src/createTableHook.ts:236](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L236) Creates a group column definition with nested child columns. The cell, header, and footer contexts include pre-bound components. diff --git a/docs/framework/lit/reference/type-aliases/AppDisplayColumnDef.md b/docs/framework/lit/reference/type-aliases/AppDisplayColumnDef.md index 7f80e00a3a..cf84752256 100644 --- a/docs/framework/lit/reference/type-aliases/AppDisplayColumnDef.md +++ b/docs/framework/lit/reference/type-aliases/AppDisplayColumnDef.md @@ -9,7 +9,7 @@ title: AppDisplayColumnDef type AppDisplayColumnDef = Omit, "cell" | "header" | "footer"> & object; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:121](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L121) +Defined in: [packages/lit-table/src/createTableHook.ts:122](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L122) Enhanced display column definition with pre-bound components. diff --git a/docs/framework/lit/reference/type-aliases/AppGroupColumnDef.md b/docs/framework/lit/reference/type-aliases/AppGroupColumnDef.md index 69c0e75c2e..70bc3f0507 100644 --- a/docs/framework/lit/reference/type-aliases/AppGroupColumnDef.md +++ b/docs/framework/lit/reference/type-aliases/AppGroupColumnDef.md @@ -9,7 +9,7 @@ title: AppGroupColumnDef type AppGroupColumnDef = Omit, "cell" | "header" | "footer" | "columns"> & object; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:144](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L144) +Defined in: [packages/lit-table/src/createTableHook.ts:145](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L145) Enhanced group column definition with pre-bound components. @@ -24,7 +24,7 @@ optional cell: AppColumnDefTemplate[]; +optional columns: ReadonlyArray>; ``` ### footer? diff --git a/docs/framework/lit/reference/type-aliases/AppHeaderContext.md b/docs/framework/lit/reference/type-aliases/AppHeaderContext.md index a2da9caa18..46e6eb056b 100644 --- a/docs/framework/lit/reference/type-aliases/AppHeaderContext.md +++ b/docs/framework/lit/reference/type-aliases/AppHeaderContext.md @@ -9,7 +9,7 @@ title: AppHeaderContext type AppHeaderContext = object; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:69](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L69) +Defined in: [packages/lit-table/src/createTableHook.ts:70](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L70) Enhanced HeaderContext with pre-bound header components. The `header` property includes the registered headerComponents. @@ -40,7 +40,7 @@ The `header` property includes the registered headerComponents. column: Column; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:75](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L75) +Defined in: [packages/lit-table/src/createTableHook.ts:76](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L76) *** @@ -50,19 +50,19 @@ Defined in: [packages/lit-table/src/createTableHook.ts:75](https://github.com/Ta header: Header & BoundComponents & object; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:76](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L76) +Defined in: [packages/lit-table/src/createTableHook.ts:77](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L77) #### Type Declaration ##### FlexRender() ```ts -FlexRender: () => TemplateResult | string | null; +FlexRender: () => LitRenderable; ``` ###### Returns -`TemplateResult` \| `string` \| `null` +[`LitRenderable`](LitRenderable.md) *** @@ -72,4 +72,4 @@ FlexRender: () => TemplateResult | string | null; table: Table; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:80](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L80) +Defined in: [packages/lit-table/src/createTableHook.ts:81](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L81) diff --git a/docs/framework/lit/reference/type-aliases/AppLitTable.md b/docs/framework/lit/reference/type-aliases/AppLitTable.md index 2d74a6aa64..040021f8e1 100644 --- a/docs/framework/lit/reference/type-aliases/AppLitTable.md +++ b/docs/framework/lit/reference/type-aliases/AppLitTable.md @@ -9,7 +9,7 @@ title: AppLitTable type AppLitTable = LitTable & NoInfer & object; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:288](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L288) +Defined in: [packages/lit-table/src/createTableHook.ts:289](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L289) Extended table API returned by useAppTable with all App wrapper functions @@ -18,7 +18,7 @@ Extended table API returned by useAppTable with all App wrapper functions ### AppCell() ```ts -AppCell: (cell, renderFn) => TemplateResult | string; +AppCell: (cell, renderFn) => LitRenderable; ``` Wraps a cell and provides cell context with pre-bound cellComponents. @@ -37,11 +37,11 @@ Wraps a cell and provides cell context with pre-bound cellComponents. ##### renderFn -(`cell`) => `TemplateResult` \| `string` +(`cell`) => [`LitRenderable`](LitRenderable.md) #### Returns -`TemplateResult` \| `string` +[`LitRenderable`](LitRenderable.md) #### Example @@ -52,7 +52,7 @@ ${table.AppCell(cell, (c) => html`${c.FlexRender()}`)} ### AppFooter() ```ts -AppFooter: (header, renderFn) => TemplateResult | string; +AppFooter: (header, renderFn) => LitRenderable; ``` Wraps a footer and provides header context with pre-bound headerComponents. @@ -71,11 +71,11 @@ Wraps a footer and provides header context with pre-bound headerComponents. ##### renderFn -(`header`) => `TemplateResult` \| `string` +(`header`) => [`LitRenderable`](LitRenderable.md) #### Returns -`TemplateResult` \| `string` +[`LitRenderable`](LitRenderable.md) #### Example @@ -86,7 +86,7 @@ ${table.AppFooter(footer, (f) => html`${f.FlexRender()}`)} ### AppHeader() ```ts -AppHeader: (header, renderFn) => TemplateResult | string; +AppHeader: (header, renderFn) => LitRenderable; ``` Wraps a header and provides header context with pre-bound headerComponents. @@ -105,11 +105,11 @@ Wraps a header and provides header context with pre-bound headerComponents. ##### renderFn -(`header`) => `TemplateResult` \| `string` +(`header`) => [`LitRenderable`](LitRenderable.md) #### Returns -`TemplateResult` \| `string` +[`LitRenderable`](LitRenderable.md) #### Example diff --git a/docs/framework/lit/reference/type-aliases/BoundComponents.md b/docs/framework/lit/reference/type-aliases/BoundComponents.md index 6e602b3638..07e84c5124 100644 --- a/docs/framework/lit/reference/type-aliases/BoundComponents.md +++ b/docs/framework/lit/reference/type-aliases/BoundComponents.md @@ -9,7 +9,7 @@ title: BoundComponents type BoundComponents = { [TKey in keyof TComponents]: () => ReturnType }; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:34](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L34) +Defined in: [packages/lit-table/src/createTableHook.ts:35](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L35) ## Type Parameters diff --git a/docs/framework/lit/reference/type-aliases/ComponentType.md b/docs/framework/lit/reference/type-aliases/ComponentType.md index 37cb672734..8bb2f9aa9f 100644 --- a/docs/framework/lit/reference/type-aliases/ComponentType.md +++ b/docs/framework/lit/reference/type-aliases/ComponentType.md @@ -9,7 +9,7 @@ title: ComponentType type ComponentType = (props) => any; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:32](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L32) +Defined in: [packages/lit-table/src/createTableHook.ts:33](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L33) ## Type Parameters diff --git a/docs/framework/lit/reference/type-aliases/CreateTableHookOptions.md b/docs/framework/lit/reference/type-aliases/CreateTableHookOptions.md index cc8073add5..d918364ed1 100644 --- a/docs/framework/lit/reference/type-aliases/CreateTableHookOptions.md +++ b/docs/framework/lit/reference/type-aliases/CreateTableHookOptions.md @@ -9,7 +9,7 @@ title: CreateTableHookOptions type CreateTableHookOptions = Omit, "columns" | "data" | "store" | "state" | "initialState"> & object; ``` -Defined in: [packages/lit-table/src/createTableHook.ts:253](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L253) +Defined in: [packages/lit-table/src/createTableHook.ts:254](https://github.com/TanStack/table/blob/main/packages/lit-table/src/createTableHook.ts#L254) Options for creating a table hook with pre-bound components and default table options. Extends all TableOptions except 'columns' | 'data' | 'store' | 'state' | 'initialState'. diff --git a/docs/framework/lit/reference/type-aliases/FlexRenderProps.md b/docs/framework/lit/reference/type-aliases/FlexRenderProps.md index 39711fc8c5..bdef5a7b3b 100644 --- a/docs/framework/lit/reference/type-aliases/FlexRenderProps.md +++ b/docs/framework/lit/reference/type-aliases/FlexRenderProps.md @@ -6,7 +6,7 @@ title: FlexRenderProps # Type Alias: FlexRenderProps\ ```ts -type FlexRenderProps = +type FlexRenderProps = | { cell: Cell; footer?: never; @@ -24,7 +24,7 @@ type FlexRenderProps = }; ``` -Defined in: [packages/lit-table/src/flexRender.ts:56](https://github.com/TanStack/table/blob/main/packages/lit-table/src/flexRender.ts#L56) +Defined in: [packages/lit-table/src/flexRender.ts:67](https://github.com/TanStack/table/blob/main/packages/lit-table/src/flexRender.ts#L67) Simplified component wrapper of `flexRender`. Use this utility function to render headers, cells, or footers with custom markup. Only one prop (`cell`, `header`, or `footer`) may be passed. diff --git a/docs/framework/lit/reference/type-aliases/LitRenderable.md b/docs/framework/lit/reference/type-aliases/LitRenderable.md new file mode 100644 index 0000000000..19fcbd1e2d --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/LitRenderable.md @@ -0,0 +1,24 @@ +--- +id: LitRenderable +title: LitRenderable +--- + +# Type Alias: LitRenderable + +```ts +type LitRenderable = + | TemplateResult + | DirectiveResult + | Node + | string + | number + | bigint + | boolean + | null + | undefined + | typeof nothing + | typeof noChange +| Iterable; +``` + +Defined in: [packages/lit-table/src/flexRender.ts:11](https://github.com/TanStack/table/blob/main/packages/lit-table/src/flexRender.ts#L11) diff --git a/docs/framework/lit/reference/type-aliases/LitTable.md b/docs/framework/lit/reference/type-aliases/LitTable.md index ac6715de90..64a961162f 100644 --- a/docs/framework/lit/reference/type-aliases/LitTable.md +++ b/docs/framework/lit/reference/type-aliases/LitTable.md @@ -9,7 +9,7 @@ title: LitTable type LitTable = Omit, "store"> & object; ``` -Defined in: [packages/lit-table/src/TableController.ts:21](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L21) +Defined in: [packages/lit-table/src/TableController.ts:26](https://github.com/TanStack/table/blob/main/packages/lit-table/src/TableController.ts#L26) The extended table type returned by the Lit adapter. Includes a `Subscribe` method for fine-grained state subscriptions diff --git a/docs/framework/lit/reference/type-aliases/SelectionSource.md b/docs/framework/lit/reference/type-aliases/SelectionSource.md index feffb121cb..fe5da2cb69 100644 --- a/docs/framework/lit/reference/type-aliases/SelectionSource.md +++ b/docs/framework/lit/reference/type-aliases/SelectionSource.md @@ -6,7 +6,7 @@ title: SelectionSource # Type Alias: SelectionSource\ ```ts -type SelectionSource = +type SelectionSource = | Atom | ReadonlyAtom | Store diff --git a/docs/framework/lit/reference/variables/subscribe.md b/docs/framework/lit/reference/variables/subscribe.md index 72c84f206b..86e819b21b 100644 --- a/docs/framework/lit/reference/variables/subscribe.md +++ b/docs/framework/lit/reference/variables/subscribe.md @@ -12,7 +12,7 @@ const subscribe: { }; ``` -Defined in: [packages/lit-table/src/subscribe-directive.ts:182](https://github.com/TanStack/table/blob/main/packages/lit-table/src/subscribe-directive.ts#L182) +Defined in: [packages/lit-table/src/subscribe-directive.ts:194](https://github.com/TanStack/table/blob/main/packages/lit-table/src/subscribe-directive.ts#L194) A Lit directive that subscribes to a source (Store or Atom) and efficiently updates only the wrapped template @@ -50,8 +50,8 @@ Subscribes to the entire source state without filtering. ```ts ( - source, - selector, + source, + selector, template): DirectiveResult; ``` diff --git a/docs/framework/octane/guide/aggregation.md b/docs/framework/octane/guide/aggregation.md new file mode 100644 index 0000000000..14715dc392 --- /dev/null +++ b/docs/framework/octane/guide/aggregation.md @@ -0,0 +1,135 @@ +--- +title: Aggregation (Octane) Guide +--- + +## Examples + +- [Aggregation](../examples/aggregation) +- [Grouped Aggregation](../examples/grouped-aggregation) + +Aggregation is independent from column grouping. Register `rowAggregationFeature` +whenever columns calculate totals or aggregated values. Add +`columnGroupingFeature` separately only when the table also groups rows. + +For the complete behavior and type reference, see the core +[Aggregation Guide](../../../guide/aggregation). + +## Aggregation Setup + +Register only the built-in functions referenced by name. Passing a definition +directly to a column does not require a registry entry. + +```tsx +import { + rowAggregationFeature, + aggregationFn_count, + aggregationFn_extent, + aggregationFn_mean, + aggregationFn_sum, + tableFeatures, + useTable, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + rowAggregationFeature, + aggregationFns: { + count: aggregationFn_count, + extent: aggregationFn_extent, + mean: aggregationFn_mean, + sum: aggregationFn_sum, + }, +}) + +const table = useTable({ + features, + columns, + data, +}) +``` + +The aggregation feature does not require a grouped row model. This makes grand +totals and custom row-subset totals available in otherwise ordinary tables. + +## Column Aggregations + +A column accepts one aggregation or an array. A single entry returns a scalar; +multiple entries return an object keyed by the aggregation name or descriptor +`id`. + +```tsx +columnHelper.accessor('amount', { + aggregationFn: 'sum', +}) + +columnHelper.accessor('score', { + aggregationFn: ['count', 'mean', { id: 'range', aggregationFn: 'extent' }], +}) +``` + +String values remain backward-compatible. Use descriptors when a result needs +a stable custom key or options. + +## Grand Totals and Row Subsets + +Call `column.getAggregationValue()` without arguments to aggregate the default +pre-grouped row model. Filtering is included; grouping, sorting, expansion, and +pagination do not change that default total. + +```tsx +footer: ({ column }) => column.getAggregationValue().toLocaleString() +``` + +Pass one options object with rows from any row model to choose a different set: + +```tsx +column.getAggregationValue({ rows: table.getCoreRowModel().rows }) +column.getAggregationValue({ rows: table.getRowModel().rows }) +column.getAggregationValue({ rows: table.getFilteredSelectedRowModel().rows }) +column.getAggregationValue({ rows: table.getCoreRowModel().rows.slice(0, 3) }) +column.getAggregationValue({ rows: table.getCoreRowModel().rows, maxDepth: 1 }) +``` + +Depth is relative to the supplied row array. `0` selects those roots, `1` +selects their direct sub-rows, and `Infinity` selects terminal rows. Configure +`maxAggregationDepth` on the column for cached default calls, or pass +`maxDepth` in the options object as an explicit override. +`table.getMaxSubRowDepth()` returns +the deepest structural depth in the core row model. Column option +`getAggregationValue(context)` can provide an external or server-computed +value; return `undefined` to fall back to the configured aggregation function. + +## Grouped Aggregation + +Grouped aggregation composes two independent features. Register both, add the +grouped row-model slot, and configure aggregation functions on the columns that +should produce grouped values. + +```tsx +const features = tableFeatures({ + rowAggregationFeature, + columnGroupingFeature, + groupedRowModel: createGroupedRowModel(), + aggregationFns: { sum: aggregationFn_sum }, +}) + +columnHelper.accessor('visits', { + aggregationFn: 'sum', + aggregatedCell: ({ getValue }) => getValue().toLocaleString(), + footer: ({ column }) => column.getAggregationValue().toLocaleString(), +}) +``` + +Use `cell.getIsAggregated()` to identify a grouped aggregate cell. Footer +rendering uses the adapter's normal footer renderer. + +## Custom Aggregation Definitions + +Use `constructAggregationFn({ aggregate, merge? })` for custom definitions. +The aggregate context includes depth-selected `rows`, `maxDepth`, `getValue`, +`column`, and `table`. Every aggregation configured on a column receives the +same row frontier. Grouped calls also include `groupingRow` and immediate +`subRows` for custom structural behavior. A `merge` implementation can more +efficiently combine already-computed sub-row results. + +See [Custom Aggregation Definitions](../../../guide/aggregation#custom-aggregation-definitions) +for the full contract, return typing, caching behavior, and worker limitations. diff --git a/docs/framework/octane/guide/cell-selection.md b/docs/framework/octane/guide/cell-selection.md new file mode 100644 index 0000000000..a04069b62d --- /dev/null +++ b/docs/framework/octane/guide/cell-selection.md @@ -0,0 +1,407 @@ +--- +title: Cell Selection (Octane) Guide +--- + +## Examples + +The cell-selection APIs work with Octane, but there is not currently a dedicated +Octane example. The existing React and Preact examples depend on an additional +TanStack framework package, so they are intentionally outside the Octane example +porting scope. + +### Cell Selection Setup + +Here's how you set up your table to use cell selection features. Adding the cell selection feature enables the related APIs. + +```ts +import { + useTable, + tableFeatures, + cellSelectionFeature, +} from '@tanstack/octane-table' + +const features = tableFeatures({ cellSelectionFeature }) + +const table = useTable({ + features, + columns, + data, +}) +``` + +## Cell Selection (Octane) Guide + +The cell selection feature keeps track of spreadsheet-style rectangular selections. A user can click a cell, drag across a block of cells, Shift-click to extend, and Ctrl/Cmd-click to add a second rectangle. Let's take a look at some common use cases. + +### Access Cell Selection State + +The table instance already manages the cell selection state for you. You can access the selection or values derived from it through a few APIs. + +- `table.state.cellSelection` - returns the cell selection state reactively (selected by the `useTable` selector) +- `getSelectedCellCount()` - returns how many cells are selected +- `getSelectedCellIds()` - returns the ids of every selected cell +- `getCellSelectionRowIds()` / `getCellSelectionColumnIds()` - returns the rows and columns the selection touches +- `getSelectedCellRangesData()` - returns each selected rectangle's values as a row-major grid + +```ts +console.log(table.state.cellSelection) //get the cell selection state +console.log(table.getSelectedCellCount()) //3 +console.log(table.getSelectedCellIds()) //['0_firstName', '0_lastName', '1_firstName'] +console.log(table.getSelectedCellRangesData()) //[[['Tanner', 'Linsley'], ['Kevin', 'Vandy']]] +``` + +In event handlers or other non-render code, you can also read the current snapshot with `table.atoms.cellSelection.get()`. This read does not subscribe a component to future changes, so prefer `table.state.cellSelection` in render positions. + +The expansion APIs (`getSelectedCellIds`, `getSelectedCellRangesData`) are memoized and pull-based. They cost nothing unless you actually call them, so a table that only highlights cells never pays to enumerate a large selection. + +### Cell Selection State Shape + +`CellSelectionState` is an array of rectangles, each stored as its two defining corners: + +```ts +type CellSelectionRange = { + anchorRowId: string + anchorColumnId: string + focusRowId: string + focusColumnId: string +} + +type CellSelectionState = Array +``` + +The `anchor` corner is where the selection started and stays put. The `focus` corner is the one that moves while dragging or Shift-extending. Storing both corners, rather than a normalized min/max rectangle, is what makes Shift-extend and "collapse back to the active cell" possible. + +Because ranges are only two corners, a drag across thousands of cells updates two strings rather than building a map with one entry per selected cell. + +### Manage Cell Selection State + +If you need access to the selection elsewhere in your application, you can own the state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. + +```ts +import { useCreateAtom } from '@tanstack/octane-store' +import { + useTable, + tableFeatures, + cellSelectionFeature, + type CellSelectionState, +} from '@tanstack/octane-table' + +const features = tableFeatures({ cellSelectionFeature }) + +const cellSelectionAtom = useCreateAtom([]) + +const table = useTable({ + features, + columns, + data, + atoms: { cellSelection: cellSelectionAtom }, +}) +``` + +The classic controlled-state pattern also works: + +```ts +const [cellSelection, setCellSelection] = useState([]) + +const table = useTable({ + features, + columns, + data, + state: { cellSelection }, + onCellSelectionChange: setCellSelection, +}) +``` + +> Note: a drag emits one change per cell boundary the pointer crosses, so `onCellSelectionChange` fires repeatedly during a drag. If you are syncing selection to a server or a URL, debounce it or commit on `mouseup`. + +### Useful Row Ids + +Cell selection is keyed by row id and column id, so a meaningful row id matters here for the same reason it does with row selection. Use the `getRowId` table option to key selection by something stable from your data. + +```ts +const table = useTable({ + features, + //... + getRowId: (row) => row.uuid, // use the row's uuid from your database as the row id +}) +``` + +### Enable Cell Selection Conditionally + +Cell selection is enabled by default for every cell. Use the `enableCellSelection` table option to turn it off entirely, or pass a function for per-cell control. + +```ts +const table = useTable({ + features, + //... + enableCellSelection: (cell) => cell.row.original.age > 18, //only adults' cells are selectable +}) +``` + +A column def can also opt out, which is the common case for checkbox or action columns. A column-level `false` wins over the table option. + +```ts +columnHelper.accessor('actions', { + enableCellSelection: false, //this column can never be selected +}) +``` + +A cell that cannot be selected is skipped even when a rectangle is drawn straight through it, and `moveCellSelection` steps over its column rather than landing on it. Use `cell.getCanSelect()` to decide whether to attach selection handlers in your UI. + +### Mouse Interactions + +Two cell handlers drive every mouse interaction: + +- `cell.getSelectionStartHandler()` - bind to `onMouseDown` +- `cell.getSelectionExtendHandler()` - bind to `onMouseEnter` + +```tsx + + + +``` + +You do not need to handle `mouseup` yourself. The start handler attaches its own document-level `mouseup` listener and removes it when the drag ends, so releasing the pointer outside the table still finishes the drag correctly. If your table renders into another document, such as an iframe or a popout window, pass that document in: `cell.getSelectionStartHandler(myDocument)`. + +#### Drag Selection + +Pressing down on a cell starts a new single-cell range, and every cell the pointer then enters moves that range's focus corner. Set `enableCellSelectionDrag: false` to require explicit clicks instead. + +#### Shift Range Selection + +Shift-clicking moves the active range's focus corner to the clicked cell, keeping its anchor fixed. The active cell therefore stays where the selection started, matching spreadsheet behavior. + +The handler recognizes Shift when the event exposes either `event.shiftKey` or `event.nativeEvent.shiftKey`. You can disable range behavior or replace the detection: + +```ts +const table = useTable({ + features, + //... + enableCellRangeSelection: false, + + // For example, use the platform modifier instead of Shift: + // isCellRangeSelectionEvent: event => Boolean(event.metaKey), +}) +``` + +#### Multiple Ranges + +Ctrl-clicking or Cmd-clicking pushes an additional rectangle onto the selection instead of replacing it. Set `enableMultiCellRangeSelection: false` to allow only one rectangle at a time, or override `isMultiCellRangeSelectionEvent` to change the modifier. + +### Render Cell Selection UI + +TanStack Table does not dictate how you render selected cells. These cell APIs give you everything you need: + +- `cell.getIsSelected()` - whether this cell falls inside any range +- `cell.getIsFocused()` - whether this is the active cell +- `cell.getSelectionEdges()` - which sides sit on the selection boundary +- `cell.getTabIndex()` - `0` for the focused cell and `-1` otherwise, for roving tabindex + +`getSelectionEdges()` returns `{ top, right, bottom, left }`, where a side is `true` when the neighboring cell in that direction is not itself selected. That is what lets you draw a single continuous outline around a selection, including around a union of separate rectangles, without every cell inspecting its neighbors. + +```tsx +function getCellClassName(cell) { + // most cells are unselected, so bail before asking for edges + if (!cell.getIsSelected()) { + return cell.getIsFocused() ? 'cell cell-focused' : 'cell' + } + + const edges = cell.getSelectionEdges() + + return [ + 'cell', + 'cell-selected', + cell.getIsFocused() && 'cell-focused', + edges.top && 'cell-edge-top', + edges.right && 'cell-edge-right', + edges.bottom && 'cell-edge-bottom', + edges.left && 'cell-edge-left', + ] + .filter(Boolean) + .join(' ') +} +``` + +> Tip: draw the outline with `box-shadow: inset ...` rather than `border`. On a `border-collapse` table a thicker border widens the shared grid line, which makes rows change height as cells become selected. A box-shadow never affects layout. + +### Keyboard Navigation + +Cell selection ships no keyboard handling of its own. Instead it exposes imperative APIs so a dedicated library, such as [TanStack Hotkeys](https://tanstack.com/hotkeys), can drive it: + +- `table.moveCellSelection(direction)` - collapse the selection to a single cell one step away +- `table.extendCellSelection(direction)` - move the active range's focus corner, keeping its anchor +- `table.setFocusedCell(rowId, columnId)` - collapse the selection to one specific cell +- `table.selectAllCells()` - select every selectable cell +- `table.resetCellSelection(true)` - clear the selection + +`direction` is `'up'`, `'down'`, `'left'`, or `'right'`. + +```ts +import { useHotkeys } from '@tanstack/octane-hotkeys' + +const gridRef = useRef(null) + +useHotkeys( + [ + { hotkey: 'ArrowUp', callback: () => table.moveCellSelection('up') }, + { hotkey: 'ArrowDown', callback: () => table.moveCellSelection('down') }, + { + hotkey: 'Shift+ArrowDown', + callback: () => table.extendCellSelection('down'), + }, + { hotkey: 'Mod+A', callback: () => table.selectAllCells() }, + { hotkey: 'Escape', callback: () => table.resetCellSelection(true) }, + ], + { target: gridRef }, +) +``` + +Scope the hotkeys to the grid element rather than the document, or arrow keys and Escape will hijack inputs elsewhere on the page. + +### Copying a Selection + +`getSelectedCellRangesData()` returns raw values indexed as `[rangeIndex][rowIndex][columnIndex]`. Turning that into clipboard text is left to your application, because the delimiter, the representation of `null`, and any quoting rules are decisions only you can make. + +```ts +function escapeTsvValue(value: unknown) { + const text = value == null ? '' : String(value) + const safeText = + typeof value === 'string' && /^[\t\r ]*[=+@-]/.test(value) + ? `'${text}` + : text + // spreadsheets expect a quoted field once it contains a delimiter, a newline, + // or a quote, with inner quotes doubled + return /["\t\n\r]/.test(safeText) + ? `"${safeText.replace(/"/g, '""')}"` + : safeText +} + +function toTsv(ranges: Array>>) { + return ranges + .map((grid) => + grid.map((row) => row.map(escapeTsvValue).join('\t')).join('\n'), + ) + .join('\n\n') +} + +navigator.clipboard.writeText(toTsv(table.getSelectedCellRangesData())) +``` + +### How Ranges Survive Table Changes + +Ranges store row and column ids, not positions, so they follow their corner cells rather than screen coordinates. + +- **Sorting, filtering, and column reordering** keep the corners pinned and recompute what sits between them. A range from "row A to row B" still runs from A to B after a sort, even though different rows now fall in between. +- **Column pinning** is accounted for in render order, so a rectangle stays visually contiguous when a column is pinned. +- **Hiding a column** that a corner sits on makes the range inert. Nothing renders as selected, but the range stays in state and comes back when the column is shown again. +- **Pagination** resolves against the pre-pagination order, so a range can span pages and lights up correctly on whichever page you are viewing. + +Because a reorder can widen a selection onto columns the user never picked, some applications prefer to clear the selection whenever the column layout changes. That is a userland decision, and one `useEffect` away: + +```ts +const isFirstLayout = useRef(true) + +useEffect(() => { + if (isFirstLayout.current) { + isFirstLayout.current = false + return + } + table.resetCellSelection(true) +}, [ + table.state.columnOrder, + table.state.columnPinning, + table.state.columnVisibility, +]) +``` + +### Resetting Cell Selection + +`table.resetCellSelection()` restores `initialState.cellSelection`. Pass `true` to ignore initial state and clear the selection entirely. + +The selection also resets automatically whenever `data` changes, because new data can invalidate the row ids a range points at, or silently re-select cells if the new data happens to reuse ids. Turn that off with `autoResetCellSelection: false`, and note that `autoResetAll` overrides it. + +```ts +const table = useTable({ + features, + //... + autoResetCellSelection: false, //keep ranges across data changes +}) +``` + +### Performance with table.Subscribe + +Octane re-renders a component subtree when its state changes, so a drag that +updates on every cell boundary can reconcile the whole table body. `table.Subscribe` +is the tool for this, and where you put it is what matters. + +Put a subscription on each row with a selector that returns only what changes +that row's appearance. `Subscribe` uses `useSelector` under the hood, so a row +whose selected value is unchanged does not re-render: + +```tsx + + {table.getRowModel().rows.map((row) => ( + + rowSelectionKey( + ranges, + table.getCellSelectionBounds(), + row.getDisplayIndex(), + row.id, + ) + } + > + {() => ( + + {row.getVisibleCells().map((cell) => ( + + + + ))} + + )} + + ))} + +``` + +The selector needs to encode whether this row's cells fall inside a range, +whether the rows immediately above and below do (that decides its top and bottom +edges), and whether it owns the focused cell. + +```ts +function rowSelectionKey(ranges, bounds, rowIndex, rowId) { + const active = ranges[ranges.length - 1] + let key = + ranges.length > 0 && active.anchorRowId === rowId + ? `f${active.anchorColumnId}` + : '' + + for (const bound of bounds) { + const self = rowIndex >= bound.minRowIndex && rowIndex <= bound.maxRowIndex + const above = + rowIndex - 1 >= bound.minRowIndex && rowIndex - 1 <= bound.maxRowIndex + const below = + rowIndex + 1 >= bound.minRowIndex && rowIndex + 1 <= bound.maxRowIndex + + if (self || above || below) { + key += `|${self ? 1 : 0}${above ? 1 : 0}${below ? 1 : 0}:${bound.minColumnIndex}-${bound.maxColumnIndex}` + } + } + + return key +} +``` + +`table.getCellSelectionBounds()` is memoized, so it computes once per selection +change no matter how many rows call it. On a thousand-row table this keeps a drag +around 10ms per update instead of reconciling every cell. + +Leave column layout out of the selector: pinning, reordering, and hiding columns +are usually part of your `useTable` selector already, so the parent re-renders +and recreates these rows anyway. diff --git a/docs/framework/octane/guide/column-faceting.md b/docs/framework/octane/guide/column-faceting.md new file mode 100644 index 0000000000..68328644f6 --- /dev/null +++ b/docs/framework/octane/guide/column-faceting.md @@ -0,0 +1,325 @@ +--- +title: Faceting (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Faceted Filters](../examples/filters-faceted) +- [Bucketed Faceted Filters](../examples/filters-faceted-bucketed) + +### Faceting Setup + +Here's how you set up your table to use faceting features. Adding the faceting feature enables the related APIs. Additionally, if using client-side faceting, you also need to set up `filteredRowModel` and `facetedRowModel` after their associated features because row model slots are type-checked. + +```tsx +import { + useTable, + tableFeatures, + columnFacetingFeature, + columnFilteringFeature, + createFacetedRowModel, + createFacetedUniqueValues, + createFacetedMinMaxValues, + createFilteredRowModel, + filterFns, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + columnFacetingFeature, + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + facetedRowModel: createFacetedRowModel(), // if using client-side faceting + facetedUniqueValues: createFacetedUniqueValues(), + facetedMinMaxValues: createFacetedMinMaxValues(), + filterFns, +}) + +const table = useTable({ + features, + columns, + data, +}) +``` + +## Faceting (Octane) Guide + +### What is Faceting? + +Faceting derives information that can be used to build filtering interfaces. For a given column, faceting can answer questions such as: + +- Which values are available? +- How often does each value occur? +- What is the minimum and maximum value among the available rows? +- Which rows should be used for a custom facet calculation? + +For example, an application could use faceting to render a plan filter like this: + +```text +Plan +☐ Free 128 +☐ Pro 47 +☐ Enterprise 9 +``` + +The plan names and counts are derived from the table's faceted row model. If a filter on another column changes, such as `Region = Europe`, the plan counts update to describe only the rows in that region. + +Faceting does not apply filters to the table. It provides values, counts, ranges, or rows that you can use to build a filter UI. The column filtering feature owns the filter state and determines which rows match the selected filter values. + +#### Faceting vs Row Aggregation + +Faceting and row aggregation both summarize data, but they serve different purposes. Faceting produces metadata for filter controls, such as available values, occurrence counts, or a numeric range. Row aggregation computes result values over a set of rows, such as a sum, average, or total, for display in footers or grouped rows. + +Faceted counts do not create aggregate rows or use a column's `aggregationFn`. A useful way to distinguish the features is: + +- Filtering answers: Which rows remain? +- Faceting answers: Which filtering choices remain? +- Row aggregation answers: What summary value can be calculated from these rows? + +### How Column Faceting Responds to Filters + +A column's faceted row model includes rows that pass every applicable filter except that column's own filter. This lets a facet continue to show alternative choices while the user edits it. + +Consider a table with `Region` and `Plan` filters: + +1. The user selects `Region = Europe`. +2. The `Plan` facet applies the region filter and recalculates its plan counts. +3. The user selects `Plan = Pro`. +4. The table displays only European Pro rows. +5. The `Plan` facet still calculates its choices from all European rows because it excludes its own `Plan` filter. + +Other facets do apply the selected plan filter. For example, a `Status` facet would now describe only European Pro rows. This is what allows multiple facets to narrow each other. + +Client-side faceting needs both `filteredRowModel` and `facetedRowModel` to provide this behavior. Without a filtered row model, the faceted row model falls back to the pre-filtered rows, so its values will not react to other column filters. + +### Faceting APIs + +Use the faceting API that matches the filter interface you are building: + +| API | Result | Common uses | +| --------------------------------- | --------------------------------------- | ------------------------------------------------------ | +| `column.getFacetedRowModel()` | Rows that pass the other active filters | Custom facet calculations | +| `column.getFacetedUniqueValues()` | A `Map` of values to occurrence counts | Checkboxes, select menus, and autocomplete suggestions | +| `column.getFacetedMinMaxValues()` | A `[min, max]` tuple or `undefined` | Number inputs and range sliders | + +The row model factories registered in `tableFeatures` enable these APIs: + +- `createFacetedRowModel()` is required for client-side faceting. +- `createFacetedUniqueValues()` is required for unique values and counts. +- `createFacetedMinMaxValues()` is required for numeric minimum and maximum values. + +Register only the factories your table uses. The complete setup near the top of this guide registers all three. + +### Unique Values and Counts + +`column.getFacetedUniqueValues()` returns a `Map` whose keys are facet values and whose values are occurrence counts. You can turn that map into a sorted list for an autocomplete or select control: + +```ts +const suggestions = Array.from(column.getFacetedUniqueValues().entries()) + .sort(([valueA], [valueB]) => String(valueA).localeCompare(String(valueB))) + .slice(0, 5_000) +``` + +Each entry contains both the value and its count: + +```tsx +return ( + +) +``` + +For a scalar column, each row normally contributes one value, so the occurrence count is also a row count. A row can contribute more than one facet value by defining the column's `getUniqueValues` option. In that case, the counts describe occurrences and their total can be greater than the number of rows. + +```tsx +columnHelper.accessor('tags', { + header: 'Tags', + getUniqueValues: (row) => row.tags, +}) +``` + +If you want each count to represent rows, make sure `getUniqueValues` returns each value no more than once per row. + +### Reactive Facet Controls in Octane + +Facet values are often read inside a filter component that receives a stable `column` object. Subscribe that component to the filter state so it updates when another facet changes. `table.Subscribe` keeps the subscription local to the component that renders the facet controls. + +```tsx +function FacetOptions({ table, column }) @{ + state.columnFilters}> + {() => ( + + )} + +} + +function FacetOptionList({ column, isSelected, toggleValue }) @{ + const values = Array.from(column.getFacetedUniqueValues().entries()) + + @for (const [value, count] of values; key String(value)) { + + } +} +``` + +The filter function for the column still determines how the selected values match rows. See the [Column Filtering Guide](./column-filtering) for filter functions and filter state, or the [Faceted Filters example](../examples/filters-faceted) for a complete implementation. + +### Minimum and Maximum Values + +`column.getFacetedMinMaxValues()` returns the numeric range available after applying the other active filters. It returns `undefined` when there are no numeric values. + +```tsx +const [min, max] = column.getFacetedMinMaxValues() ?? [0, 1] + +return ( + + column.setFilterValue(Number(event.currentTarget.value)) + } + /> +) +``` + +The minimum and maximum describe the values that are available to the filter UI. Your column's filter function determines how a selected value or range filters rows. + +### Bucketed Faceting for Continuous Values + +Raw unique values are not always useful. Dates, file sizes, durations, prices, and measurements can produce hundreds or thousands of distinct values. These columns are often easier to filter when their values are placed into meaningful buckets: + +```text +Last login +☐ Today +☐ Yesterday +☐ This week +☐ This month +☐ Older +``` + +You can use the column's `getUniqueValues` option to return a bucket key for faceting while keeping the original accessor value for rendering and other table features. + +```tsx +type StorageBucket = + | 'under-1-gb' + | '1-to-10-gb' + | '10-to-100-gb' + | '100-gb-plus' + +const GB = 1024 ** 3 + +function getStorageBucket(value: number): StorageBucket { + if (value < GB) return 'under-1-gb' + if (value < 10 * GB) return '1-to-10-gb' + if (value < 100 * GB) return '10-to-100-gb' + return '100-gb-plus' +} + +const storageBucketFilter = constructFilterFn({ + resolveDataValue: (value) => getStorageBucket(value as number), + filter: (bucket, selected: Array) => selected.includes(bucket), + autoRemove: (selected: Array) => selected.length === 0, +}) + +columnHelper.accessor('storageBytes', { + header: 'Storage', + getUniqueValues: (row) => [getStorageBucket(row.storageBytes)], + filterFn: storageBucketFilter, +}) +``` + +Faceting and filtering should use the same bucket definitions so the displayed counts match the rows selected by each bucket. The column keeps its raw numeric value, so there is no need to create a hidden derived column only for faceting. See the [Bucketed Faceted Filters example](../examples/filters-faceted-bucketed) for complete date and storage bucket filters. + +### Client-Side Faceting and Performance + +The built-in client-side faceting row models are memoized. They recalculate when their input rows or relevant filter state changes. The cost still depends on the number of rows, columns, and unique values in the table. + +For columns with many unique values, consider these options: + +- Render only the first or most relevant values instead of every map entry. +- Let users search the available values before rendering a long list. +- Bucket continuous or high-cardinality values into useful ranges. +- Move faceting to the server when the complete dataset is not available in the browser. + +Avoid sorting or converting a large facet map repeatedly in unrelated components. Derive and render facet options close to the component that subscribes to the relevant filter state. + +### Custom Server-Side Faceting + +When filtering is performed on the server, the rows loaded into the browser may not contain enough information to calculate complete facet values or counts. In that case, calculate the facets on the server and provide custom `facetedUniqueValues` and `facetedMinMaxValues` factories. + +Each factory receives the table and a column ID, then returns a function that resolves the faceted result. The regular column APIs will return the server-provided values. + +```ts +const facetingData = await fetchFacets(activeFilters) + +const features = tableFeatures({ + columnFacetingFeature, + facetedUniqueValues: (_table, columnId) => () => { + const uniqueValueMap = new Map() + // Populate the map from facetingData for columnId. + return uniqueValueMap + }, + facetedMinMaxValues: (_table, columnId) => () => { + // Read the range from facetingData for columnId. + return [min, max] + }, +}) + +const table = useTable({ + features, + columns, + data, +}) +``` + +To match the built-in column faceting behavior, a server query for one column should apply the other active filters but exclude that column's own filter. This keeps alternative choices available in the current facet while allowing facets to narrow each other. + +You can also fetch facet values and pass them directly to your filter components without using the TanStack Table faceting APIs. + +### Global Faceting + +Global faceting derives values across every leaf column that can participate in global filtering. It is useful for autocomplete suggestions or other metadata associated with a global filter. The global faceted row model applies active column filters and excludes the global filter itself. + +If the table uses global filtering, register `globalFilteringFeature` so the row filtering pipeline evaluates the global filter. The same faceting factories used by column faceting also power these table APIs: + +```ts +const globalFacetedRows = table.getGlobalFacetedRowModel().flatRows + +const suggestions = Array.from(table.getGlobalFacetedUniqueValues().entries()) + +const [min, max] = table.getGlobalFacetedMinMaxValues() ?? [0, 1] +``` + +Custom faceting factories receive the internal `__global__` column ID for global requests. You can branch on that ID when the server returns separate column and global facet results: + +```ts +const features = tableFeatures({ + columnFacetingFeature, + facetedUniqueValues: (_table, columnId) => () => { + if (columnId === '__global__') { + return new Map(globalFacets.uniqueValues) + } + + return new Map(columnFacets[columnId]?.uniqueValues) + }, +}) +``` diff --git a/docs/framework/octane/guide/column-filtering.md b/docs/framework/octane/guide/column-filtering.md new file mode 100644 index 0000000000..df4a18a71b --- /dev/null +++ b/docs/framework/octane/guide/column-filtering.md @@ -0,0 +1,466 @@ +--- +title: Column Filtering (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Column Filters](../examples/filters) +- [Faceted Filters](../examples/filters-faceted) +- [Bucketed Faceted Filters](../examples/filters-faceted-bucketed) +- [Fuzzy Search](../examples/filters-fuzzy) + +### Column Filtering Setup + +Here's how you set up your table to use column filtering features. Adding the column filtering feature enables the related APIs. Additionally, if using client-side filtering, you also need to set up `filteredRowModel` after its associated feature because row model slots are type-checked. + +```tsx +import { + useTable, + tableFeatures, + columnFilteringFeature, + createFilteredRowModel, + filterFn_includesString, + filterFn_inNumberRange, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +const table = useTable({ + features, + columns, + data, +}) +``` + +> **Note:** The `filterFns` registry above lists only the built-in filter functions this table uses. Spreading the entire built-in `filterFns` registry (`filterFns: { ...filterFns }`) still works, but it puts every built-in filter function in your bundle. Register just the functions you use, or pass a function directly to the `filterFn` column option with no registration at all. + +## Column Filtering (Octane) 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 `tableFeatures`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```tsx +const features = tableFeatures({ columnFilteringFeature }) // no filteredRowModel for manual server-side filtering + +const table = useTable({ + features, + data, + columns, + manualFiltering: true, +}) +``` + +> **Note:** When using manual filtering, many of the options that are discussed in the rest of this guide will have no effect. When `manualFiltering` is set to `true`, the table instance will not apply any filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the `data` that you pass to it as-is. + +### Client-Side Filtering + +If you are using the built-in client-side filtering features, add the `columnFilteringFeature`, the `filteredRowModel` factory, and a `filterFns` registry to your `tableFeatures` call. Import `createFilteredRowModel` and the filter functions you need from TanStack Table: + +```tsx +import { + useTable, + tableFeatures, + columnFilteringFeature, + createFilteredRowModel, + filterFn_includesString, + filterFn_inNumberRange, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +const table = useTable({ + features, + data, + columns, +}) +``` + +### Column Filter State + +Whether or not you use client-side or server-side filtering, you can take advantage of the built-in column filter state management that TanStack Table provides. There are many table and column APIs to mutate and interact with the filter state and retrieving the column filter state. + +The column filtering state is defined as an array of objects with the following shape: + +```ts +interface ColumnFilter { + id: string + value: unknown +} +type ColumnFiltersState = ColumnFilter[] +``` + +Since the column filter state is an array of objects, you can have multiple column filters applied at once. + +#### Accessing Column Filter State + +For reactive reads that should re-render your UI, use `table.state.columnFilters` (selected by the `useTable` selector) or `table.Subscribe`. In event handlers or other non-render code, you can read the current snapshot with `table.atoms.columnFilters.get()`, but this read does not subscribe the component to future changes. + +```tsx +const table = useTable({ + features, + columns, + data, + //... +}) + +table.state.columnFilters // reactive read in render +table.atoms.columnFilters.get() // snapshot read in event handlers +``` + +However, if you need access to the column filter state outside of the table, you can "control" the column filter state like down below. + +### Controlled Column Filter State + +If you need easy access to the column filter state in other parts of your application, you can own the column filter state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. Atoms preserve fine-grained subscriptions, and the filter values can be used elsewhere (such as in a query key for server-side filtering) without forcing the component that owns the table to re-render. + +```tsx +import { useCreateAtom, useSelector } from '@tanstack/octane-store' + +const columnFiltersAtom = useCreateAtom([]) // can set initial column filter state here + +// subscribe to the atom wherever you need the value (e.g. for a query key) +const columnFilters = useSelector(columnFiltersAtom) + +const table = useTable({ + features, + columns, + data, + //... + atoms: { + columnFilters: columnFiltersAtom, // table filter APIs now update columnFiltersAtom + }, +}) +``` + +Alternatively, the v8-style `state.columnFilters` plus `onColumnFiltersChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```tsx +const [columnFilters, setColumnFilters] = useState([]) +//... +const table = useTable({ + features, + 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, + columns, + data, + //... + initialState: { + columnFilters: [ + { + id: 'name', + value: 'John', // filter the name column by 'John' by default + }, + ], + }, +}) +``` + +> **NOTE**: Do not use both `initialState.columnFilters` and `state.columnFilters` at the same time, as the controlled `state.columnFilters` value will override the `initialState.columnFilters`. + +### FilterFns + +Each column can have its own unique filtering logic. Choose from any of the filter functions that are provided by TanStack Table, or create your own. + +By default there are 18 built-in filter functions to choose from: + +- `includesString` - Case-insensitive string inclusion +- `includesStringSensitive` - Case-sensitive string inclusion +- `startsWith` - Case-insensitive string prefix match +- `endsWith` - Case-insensitive string suffix match +- `equalsString` - Case-insensitive string equality +- `equalsStringSensitive` - Case-sensitive string equality +- `equals` - Strict equality `===` +- `weakEquals` - Weak equality `==` +- `empty` - The row's value is nullish or whitespace-only (the filter value is an on/off flag) +- `notEmpty` - The row's value is not nullish or whitespace-only (the filter value is an on/off flag) +- `arrIncludes` - The row's array (or string) value includes at least one of the filter values +- `arrIncludesAll` - The row's array value includes every filter value +- `arrIncludesSome` - The row's array value includes at least one of the filter values +- `arrHas` - The row's scalar value equals at least one of the filter values +- `inNumberRange` - Inclusive `[min, max]` number range (endpoints normalized and swapped if reversed) +- `inDateRange` - Inclusive `[min, max]` date range accepting `Date` objects, timestamps, or date strings (blank endpoints are open-ended) +- `between` - Exclusive min/max range (blank endpoints are open-ended) +- `betweenInclusive` - Inclusive min/max range (blank endpoints are open-ended) + +You can also define your own custom filter functions, either inline as the `filterFn` column option, or by name in the `filterFns` registry slot on `tableFeatures`. + +#### Custom Filter Functions + +> **Note:** These filter functions only run during client-side filtering. + +Whether you register a custom filter function in the `filterFns` slot on `tableFeatures` or pass it directly as a `filterFn` column option, it should have the following signature: + +```ts +const myCustomFilterFn: FilterFn = ( + row, // Row + columnId: string, + filterValue: any, + addMeta?: (meta: FilterMeta) => void, +): boolean => ... +``` + +Every filter function receives: + +- The row to filter +- The columnId to use to retrieve the row's value +- The filter value + +and should return `true` if the row should be included in the filtered rows, and `false` if it should be removed. + +```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', // reference a custom filter function registered in features + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom filter function directly + filterFn: (row, columnId, filterValue) => { + return // true or false based on your custom logic + }, + }, +] +//... +const myCustomFilterFn: FilterFn = ( + row, + columnId, + filterValue, +) => { + return // true or false based on your custom logic +} + +const features = tableFeatures({ + columnFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + myCustomFilterFn, + startsWith: startsWithFilterFn, // defined elsewhere + }, +}) + +const table = useTable({ + features, + columns, + data, +}) +``` + +> **TypeScript Note:** For `filterFn: 'myCustomFilterFn'` string references to typecheck, register the function in the `filterFns` slot on `tableFeatures` (as shown above). TypeScript infers the registered names from the slot automatically. Alternatively, skip the registry entirely by passing the function directly to the `filterFn` column option. See the [Fuzzy Search example](../examples/filters-fuzzy) for a complete registration example. + +##### Customize Filter Function Behavior + +You can attach a few other properties to filter functions to customize their behavior: + +- `filterFn.resolveFilterValue` - This optional "hanging" method on any given `filterFn` allows the filter function to transform/sanitize/format the filter value before it is passed to the filter function. The table applies it once per filter (not once per row), so it is also the right place for expensive preparation work. + +- `filterFn.resolveDataValue` - This optional "hanging" method normalizes each row's value before it is compared against the filter value. It is honored by every filter function built with the `constructFilterFn` helper, which includes all built-in filter functions. + +- `filterFn.autoRemove` - This optional "hanging" method on any given `filterFn` is passed a filter value and expected to return `true` if the filter value should be removed from the filter state. eg. Some boolean-style filters may want to remove the filter value from the table state if the filter value is set to `false`. + +The `constructFilterFn` helper builds a filter function from a value-level comparator plus those optional resolvers: + +```tsx +const startsWithFilterFn = constructFilterFn({ + // compare the (resolved) row value against the (resolved) filter value + filter: (dataValue, filterValue) => + Boolean(dataValue?.startsWith(filterValue)), + // normalize the filter value once, before any rows are tested + resolveFilterValue: (value) => String(value).toLowerCase().trim(), + // normalize each row's value before it reaches the comparator + resolveDataValue: (value) => String(value ?? '').toLowerCase(), + // remove the filter value from filter state if it is falsy (empty string in this case) + autoRemove: (value) => !value, +}) +``` + +Keeping the comparison in `filter` and the normalization in the resolvers pays off when you need a variant of an existing filter function: the definition is attached to the returned function, so you can spread any filter function built with `constructFilterFn` and override only what differs. For example, a version of `includesString` that also ignores diacritics (so a search for "eric" matches "Éric"): + +```tsx +const normalize = (value: unknown) => + String(value ?? '') + .toLowerCase() + .normalize('NFD') + .replace(/\p{Diacritic}/gu, '') + +const includesStringIgnoreDiacritics = constructFilterFn({ + ...filterFn_includesString, // reuse the comparator and autoRemove behavior + resolveFilterValue: normalize, + resolveDataValue: normalize, +}) +``` + +Register the variant by name in the `filterFns` registry or pass it directly to the `filterFn` column option, just like any other custom filter function. + +> **Note:** The table applies `resolveFilterValue` once per filter before any rows are tested. If you ever call a filter function directly (outside of a table), resolve the filter value yourself: `myFilterFn(row, columnId, myFilterFn.resolveFilterValue?.(rawValue) ?? rawValue)`. + +### Customize Column Filtering + +There are a lot of table and column options that you can use to further customize the column filtering behavior. + +#### Disable Column Filtering + +By default, column filtering is enabled for all columns. You can disable the column filtering for all columns or for specific columns by using the `enableColumnFilters` table option or the `enableColumnFilter` column option. You can also turn off both column and global filtering by setting the `enableFilters` table option to `false`. + +Disabling column filtering for a column will cause the `column.getCanFilter` API to return `false` for that column. + +```tsx +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableColumnFilter: false, // disable column filtering for this column + }, + //... +] +//... +const table = useTable({ + features, + columns, + data, + enableColumnFilters: false, // disable column filtering for all columns +}) +``` + +#### Filtering Sub-Rows (Expanding) + +There are a few additional table options to customize the behavior of column filtering when using features like expanding, grouping, and aggregation. + +##### Filter From Leaf Rows + +By default, filtering is done from parent rows down, so if a parent row is filtered out, all of its child sub-rows will be filtered out as well. Depending on your use-case, this may be the desired behavior if you only want the user to be searching through the top-level rows, and not the sub-rows. This is also the most performant option. + +However, if you want to allow sub-rows to be filtered and searched through, regardless of whether the parent row is filtered out, you can set the `filterFromLeafRows` table option to `true`. Setting this option to `true` will cause filtering to be done from leaf rows up, which means parent rows will be included so long as one of their child or grand-child rows is also included. + +```tsx +const features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +const table = useTable({ + features, + columns, + data, + filterFromLeafRows: true, // filter and search through sub-rows +}) +``` + +##### Max Leaf Row Filter Depth + +By default, filtering is done for all rows in a tree, no matter if they are root level parent rows or the child leaf rows of a parent row. Setting the `maxLeafRowFilterDepth` table option to `0` will cause filtering to only be applied to the root level parent rows, with all sub-rows remaining unfiltered. Similarly, setting this option to `1` will cause filtering to only be applied to child leaf rows 1 level deep, and so on. + +Use `maxLeafRowFilterDepth: 0` if you want to preserve a parent row's sub-rows from being filtered out while the parent row is passing the filter. + +```tsx +const features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +const table = useTable({ + features, + columns, + data, + maxLeafRowFilterDepth: 0, // only filter root level parent rows out +}) +``` + +### Column Filter APIs + +There are a lot of Column and Table APIs that you can use to interact with the column filter state and hook up to your UI components. Here is a list of the available APIs and their most common use-cases: + +- `table.setColumnFilters` - Overwrite the entire column filter state with a new state. +- `table.resetColumnFilters` - Useful for a "clear all/reset filters" button. + +- **`column.getFilterValue`** - Useful for getting the default initial filter value for an input, or even directly providing the filter value to a filter input. +- **`column.setFilterValue`** - Useful for connecting filter inputs to their `onInput` 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/octane/guide/column-ordering.md b/docs/framework/octane/guide/column-ordering.md new file mode 100644 index 0000000000..6fc2d9bff3 --- /dev/null +++ b/docs/framework/octane/guide/column-ordering.md @@ -0,0 +1,197 @@ +--- +title: Column Ordering (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Column Ordering](../examples/column-ordering) + +### Column Ordering Setup + +Here's how you set up your table to use column ordering features. Adding the column ordering feature enables the related APIs. + +```tsx +import { + useTable, + tableFeatures, + columnOrderingFeature, +} from '@tanstack/octane-table' + +const features = tableFeatures({ columnOrderingFeature }) + +const table = useTable({ + features, + columns, + data, +}) +``` + +## Column Ordering (Octane) Guide + +By default, columns are ordered in the order they are defined in the `columns` array. However, you can manually specify the column order using the `columnOrder` state. Other features like column pinning and grouping can also affect the column order. + +### What Affects Column Order + +There are 3 table features that can reorder columns, which happen in the following order: + +1. [Column Pinning](./column-pinning) - If pinning, columns are split into start, center (unpinned), and end pinned columns. +2. Manual **Column Ordering** - A manually specified column order is applied. +3. [Grouping](./grouping) - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +> **Note:** `columnOrder` state will only affect unpinned columns if used in conjunction with column pinning. + +### Column Order State + +If you don't provide a `columnOrder` state, TanStack Table will just use the order of the columns in the `columns` array. However, you can provide an array of string column ids to the `columnOrder` state to specify the order of the columns. + +#### Default Column Order + +If all you need to do is specify the initial column order, you can just specify the `columnOrder` state in the `initialState` table option. + +```tsx +const features = tableFeatures({ columnOrderingFeature }) + +const table = useTable({ + features, + //... + initialState: { + columnOrder: ['columnId1', 'columnId2', 'columnId3'], + }, + //... +}) +``` + +> **Note:** If you are using the `state` table option to also specify the `columnOrder` state, the `initialState` will have no effect. Only specify particular states in either `initialState` or `state`, not both. + +#### Managing Column Order State + +If you need to dynamically change the column order, or set the column order after the table has been initialized, you can manage the `columnOrder` state just like any other table state. + +In v9, the recommended way to own a state slice is with an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the column order without re-rendering the component that owns the table. + +```tsx +import { useCreateAtom, useSelector } from '@tanstack/octane-store' +import { + useTable, + tableFeatures, + columnOrderingFeature, +} from '@tanstack/octane-table' +import type { ColumnOrderState } from '@tanstack/octane-table' + +const features = tableFeatures({ columnOrderingFeature }) + +const columnOrderAtom = useCreateAtom([ + 'columnId1', + 'columnId2', + 'columnId3', +]) + +const columnOrder = useSelector(columnOrderAtom) // subscribe wherever it is needed + +const table = useTable({ + features, + //... + atoms: { + columnOrder: columnOrderAtom, + }, + //... +}) +``` + +Alternatively, the v8-style `state.columnOrder` plus `onColumnOrderChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```tsx +const features = tableFeatures({ columnOrderingFeature }) + +const [columnOrder, setColumnOrder] = useState([ + 'columnId1', + 'columnId2', + 'columnId3', +]) +//... +const table = useTable({ + features, + //... + state: { + columnOrder, + //... + }, + onColumnOrderChange: setColumnOrder, + //... +}) +``` + +### Reordering Columns + +If the table has UI that allows the user to reorder columns, hook the drop event of your drag-and-drop solution up to `table.setColumnOrder`. With native browser drag events, track which column is being dragged and splice it into place when it is dropped on a target column: + +```tsx +import { useState } from 'octane/hooks' + +const [draggedColumnId, setDraggedColumnId] = useState(null) + +// reorder columns after drag & drop +const handleDrop = (targetColumnId: string) => { + if (!draggedColumnId || draggedColumnId === targetColumnId) return + table.setColumnOrder((prevColumnOrder) => { + const newColumnOrder = [...prevColumnOrder] + const [movedColumnId] = newColumnOrder.splice( + newColumnOrder.indexOf(draggedColumnId), + 1, + ) + newColumnOrder.splice( + newColumnOrder.indexOf(targetColumnId), + 0, + movedColumnId, + ) + return newColumnOrder + }) + setDraggedColumnId(null) +} + +// wire up onDragStart={() => setDraggedColumnId(header.column.id)}, +// onDragOver={(e) => e.preventDefault()}, and +// onDrop={() => handleDrop(header.column.id)} on your elements +``` + +If you use a drag-and-drop library instead, call `table.setColumnOrder` from its drop callback in the same way (most libraries provide an `arrayMove`-style utility for the splice logic). + +`table.setColumnOrder` works the same whether the table manages the `columnOrder` state internally, you control it with `state` + `onColumnOrderChange`, or you own it with an external atom. + +### Column Ordering APIs + +Use `table.setColumnOrder` to update the column order state directly. Use `table.resetColumnOrder` to reset the order to `initialState.columnOrder`, or pass `true` to clear the order state. + +```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('start') +column.getIndex('center') +column.getIndex('end') + +column.getIsFirstColumn() +column.getIsLastColumn() +``` + +These helpers are useful for styling column boundaries or building drag-and-drop targets that need to know the current rendered order. + +#### Drag and Drop Column Reordering Suggestions (Octane) + +TanStack Table is not opinionated about which drag-and-drop solution you use. There is no official Octane DnD example yet, but here are a few suggestions: + +1. Native browser drag events (`onDragStart`, `onDragOver`, `onDrop`) with a little of your own state are often the simplest fit for Octane. This approach has zero dependencies and works directly with Octane's event handling, but you will need to do extra work for proper touch support on mobile. [Material React Table](https://www.material-react-table.com/docs/examples/column-ordering) implements TanStack Table column ordering this way with no DnD dependencies, and its splice logic translates directly to Octane. + +2. [Pragmatic drag and drop](https://atlassian.design/components/pragmatic-drag-and-drop/about) has a framework-agnostic core that attaches to plain DOM elements, so it works in Octane apps without any compatibility layer. + +3. The official React [Column DnD example](https://tanstack.com/table/latest/docs/framework/react/examples/column-dnd) is a useful reference for the `arrayMove` + `table.setColumnOrder` pattern, but its React-specific DnD dependency is not an Octane recommendation and the example is not ported. + +4. If you evaluate another DnD library, verify that it supports framework-agnostic DOM integration, touch and keyboard input, and semantic `` markup before committing to it. diff --git a/docs/framework/octane/guide/column-pinning.md b/docs/framework/octane/guide/column-pinning.md new file mode 100644 index 0000000000..dc9b05d13b --- /dev/null +++ b/docs/framework/octane/guide/column-pinning.md @@ -0,0 +1,195 @@ +--- +title: Column Pinning (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Column Pinning](../examples/column-pinning) +- [Column Pinning Split](../examples/column-pinning-split) +- [Sticky Column Pinning](../examples/column-pinning-sticky) + +### Column Pinning Setup + +Here's how you set up your table to use column pinning features. Adding the column pinning feature enables the related APIs. + +```tsx +import { + useTable, + tableFeatures, + columnPinningFeature, +} from '@tanstack/octane-table' + +const features = tableFeatures({ columnPinningFeature }) + +const table = useTable({ + features, + columns, + data, +}) +``` + +## Column Pinning (Octane) Guide + +TanStack Table offers state and APIs helpful for implementing column pinning features in your table UI. You can implement column pinning in multiple ways. You can either split pinned columns into their own separate tables, or you can keep all columns in the same table, but use the pinning state to order the columns correctly and use sticky CSS to pin the columns to the start or end. + +`start` and `end` are logical pinning regions. In LTR languages/layouts, `start` usually corresponds to left and `end` to right. In RTL languages/layouts, `start` usually corresponds to right and `end` to left. + +### How Column Pinning Affects Column Order + +There are 3 table features that can reorder columns, which happen in the following order: + +1. **Column Pinning** - If pinning, columns are split into start, center (unpinned), and end pinned columns. +2. Manual [Column Ordering](./column-ordering) - A manually specified column order is applied. +3. [Grouping](./grouping) - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +The only way to change the order of the pinned columns is in the `columnPinning.start` and `columnPinning.end` state itself. `columnOrder` state will only affect the order of the unpinned ("center") columns. + +### Column Pinning State + +Managing the `columnPinning` state is optional, and usually not necessary unless you are adding persistent state features. TanStack Table will already keep track of the column pinning state for you. Manage the `columnPinning` state just like any other table state if you need to. + +In v9, the recommended way to own a state slice is with an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the pinning state without re-rendering the component that owns the table. + +```tsx +import { useCreateAtom, useSelector } from '@tanstack/octane-store' +import { + useTable, + tableFeatures, + columnPinningFeature, +} from '@tanstack/octane-table' +import type { ColumnPinningState } from '@tanstack/octane-table' + +const features = tableFeatures({ columnPinningFeature }) + +const columnPinningAtom = useCreateAtom({ + start: [], + end: [], +}) + +const columnPinning = useSelector(columnPinningAtom) // subscribe wherever it is needed + +const table = useTable({ + features, + //... + atoms: { + columnPinning: columnPinningAtom, + }, + //... +}) +``` + +Alternatively, the v8-style `state.columnPinning` plus `onColumnPinningChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```tsx +const [columnPinning, setColumnPinning] = useState({ + start: [], + end: [], +}) + +const table = useTable({ + features, + //... + 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, + //... + initialState: { + columnPinning: { + start: ['expand-column'], + end: ['actions-column'], + }, + //... + }, + //... +}) +``` + +### Useful Column Pinning APIs + +> Note: These APIs are available when using `columnPinningFeature`. + +There are a handful of useful Column API methods to help you implement column pinning features: + +- `column.getCanPin`: Use to determine if a column can be pinned. +- `column.pin`: Use to pin a column to the start or end. Or use to unpin a column. +- `column.getIsPinned`: Use to determine where a column is pinned. +- `column.getPinnedIndex`: Use to read the column's index within its pinned column group. +- `column.getStart`: Use to provide the correct `start` CSS value for a pinned column. +- `column.getAfter`: Use to provide the correct `end` CSS value for a pinned column. +- `column.getIsLastColumn`: Use to determine if a column is the last column in its pinned group. Useful for adding a box-shadow. +- `column.getIsFirstColumn`: Use to determine if a column is the first column in its pinned group. Useful for adding a box-shadow. + +Use `table.setColumnPinning` to update the pinning state directly. Use `table.resetColumnPinning` to reset to `initialState.columnPinning`, or pass `true` to clear both pinned column arrays. + +```tsx +table.setColumnPinning({ + start: ['firstName'], + end: ['actions'], +}) + +table.resetColumnPinning() +table.resetColumnPinning(true) +``` + +The table instance exposes pinned column and header helpers for each region: + +```tsx +table.getStartLeafColumns() +table.getCenterLeafColumns() +table.getEndLeafColumns() + +table.getStartVisibleLeafColumns() +table.getCenterVisibleLeafColumns() +table.getEndVisibleLeafColumns() + +table.getStartHeaderGroups() +table.getCenterHeaderGroups() +table.getEndHeaderGroups() + +table.getStartFooterGroups() +table.getCenterFooterGroups() +table.getEndFooterGroups() + +table.getStartFlatHeaders() +table.getCenterFlatHeaders() +table.getEndFlatHeaders() + +table.getStartLeafHeaders() +table.getCenterLeafHeaders() +table.getEndLeafHeaders() +``` + +You can also request pinned leaf columns by region with `table.getPinnedLeafColumns(position)` and visible pinned leaf columns with `table.getPinnedVisibleLeafColumns(position)`. + +```tsx +table.getPinnedLeafColumns('start') +table.getPinnedLeafColumns('center') +table.getPinnedLeafColumns('end') + +table.getPinnedVisibleLeafColumns('start') +table.getPinnedVisibleLeafColumns('center') +table.getPinnedVisibleLeafColumns('end') +``` + +Use `table.getIsSomeColumnsPinned()` to check if any columns are pinned, or pass `'start'` or `'end'` to check one pinned side. + +### Split Table Column Pinning + +If you are just using sticky CSS to pin columns, you can for the most part, just render the table as you normally would with the `table.getHeaderGroups` and `row.getVisibleCells` methods. + +However, if you are splitting up pinned columns into their own separate tables, you can make use of the `table.getStartHeaderGroups`, `table.getCenterHeaderGroups`, `table.getEndHeaderGroups`, `row.getStartVisibleCells`, `row.getCenterVisibleCells`, and `row.getEndVisibleCells` methods to only render the columns that are relevant to the current table. diff --git a/docs/framework/octane/guide/column-resizing.md b/docs/framework/octane/guide/column-resizing.md new file mode 100644 index 0000000000..bed5df02c5 --- /dev/null +++ b/docs/framework/octane/guide/column-resizing.md @@ -0,0 +1,249 @@ +--- +title: Column Resizing (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Column Resizing](../examples/column-resizing) +- [Performant Column Resizing](../examples/column-resizing-performant) + +### Column Resizing Setup + +Here's how you set up your table to use column resizing features. Column resizing depends on column sizing, so add `columnSizingFeature` before `columnResizingFeature`. Adding the column resizing feature enables the related APIs. + +```tsx +import { + useTable, + tableFeatures, + columnSizingFeature, + columnResizingFeature, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) + +const table = useTable({ + features, + columns, + data, +}) +``` + +## Column Resizing (Octane) Guide + +TanStack Table provides built-in column resizing state and APIs that allow you to easily implement column resizing in your table UI with a variety of options for UX and performance. + +Column resizing builds on column sizing. If you only need to define starting, minimum, or maximum widths, see the [Column Sizing Guide](./column-sizing). + +### Enable Column Resizing + +To use column resizing, add `columnSizingFeature` and then `columnResizingFeature` to your features. The `column.getCanResize()` API will return `true` by default for all columns, but you can either disable column resizing for all columns with the `enableColumnResizing` table option, or disable column resizing on a per-column basis with the `enableResizing` column option. + +```tsx +import { + columnResizingFeature, + columnSizingFeature, + tableFeatures, + useTable, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + columnSizingFeature, + columnResizingFeature, +}) + +const columns = [ + { + accessorKey: 'id', + enableResizing: false, // disable resizing for just this column + size: 200, // starting column size + }, + //... +] + +const table = useTable({ + features, + columns, + data, +}) +``` + +### Column Resize Mode + +By default, the column resize mode is set to `"onEnd"`. This means that the `column.getSize()` API will not return the new column size until the user has finished resizing (dragging) the column. Usually a small UI indicator will be displayed while the user is resizing the column. + +In the Octane 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 Octane 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/octane/guide/expanding.md b/docs/framework/octane/guide/expanding.md new file mode 100644 index 0000000000..d5051d1fcd --- /dev/null +++ b/docs/framework/octane/guide/expanding.md @@ -0,0 +1,330 @@ +--- +title: Expanding (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Expanding](../examples/expanding) + +### Expanding Setup + +Here's how you set up your table to use expanding features. Adding the expanding feature enables the related APIs. Additionally, if using client-side expanding, you also need to set up `expandedRowModel` after its associated feature because row model slots are type-checked. + +```tsx +import { + useTable, + tableFeatures, + rowExpandingFeature, + createExpandedRowModel, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + rowExpandingFeature, + expandedRowModel: createExpandedRowModel(), // if using client-side expanding +}) + +const table = useTable({ + features, + columns, + data, +}) +``` + +## Expanding Feature (Octane) Guide + +Expanding is a feature that allows you to show and hide additional rows of data related to a specific row. This can be useful in cases where you have hierarchical data and you want to allow users to drill down into the data from a higher level. Or it can be useful for showing additional information related to a row. + +### Different use cases for Expanding Features + +There are multiple use cases for expanding features in TanStack Table that will be discussed below. + +1. Expanding sub-rows (child rows, aggregate rows, etc.) +2. Expanding custom UI (detail panels, sub-tables, etc.) + +### Enable Client-Side Expanding + +To use the client-side expanding features, add the `rowExpandingFeature` and the `expandedRowModel` factory to your `tableFeatures` call: + +```ts +import { + useTable, + tableFeatures, + rowExpandingFeature, + createExpandedRowModel, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + rowExpandingFeature, + expandedRowModel: createExpandedRowModel(), +}) + +const table = useTable({ + features, + // other options... +}) +``` + +Expanded data can either contain table rows or any other data you want to display. We will discuss how to handle both cases in this guide. + +### Table rows as expanded data + +Expanded rows are 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, + 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 'octane' + +//... +const table = useTable({ + features, + 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() && ( + + {/* span however many columns the expanded data needs if it does not share the parent row's columns */} + + + )} + + ))} + +//... +``` + +### Expanded rows state + +If you need access to the expanded state of the rows in other parts of your application, you can own the `expanded` state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. Atoms preserve fine-grained subscriptions, and the expanded value can be read anywhere in your app without forcing the component that owns the table to re-render. + +```ts +import { useCreateAtom, useSelector } from '@tanstack/octane-store' + +const expandedAtom = useCreateAtom({}) + +// subscribe to the atom wherever you need the value +const expanded = useSelector(expandedAtom) + +const table = useTable({ + features, + // other options... + atoms: { + expanded: expandedAtom, // expanding APIs now update expandedAtom + }, +}) +``` + +Alternatively, the v8-style `state.expanded` plus `onExpandedChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const [expanded, setExpanded] = useState({}) + +const table = useTable({ + features, + // 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 features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + filterFns, +}) + +//... +const table = useTable({ + features, + getSubRows: (row) => row.subRows, + filterFromLeafRows: true, // search through the expanded rows + maxLeafRowFilterDepth: 1, // limit the depth of the expanded rows that are searched + // other options... +}) +``` + +### Paginating Expanded Rows + +By default, expanded rows are paginated along with the rest of the table (which means expanded rows may span multiple pages). If you want to disable this behavior (which means expanded rows will always render on their 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, + // other options... + paginateExpandedRows: false, +}) +``` + +### Pinning Expanded Rows + +Pinning expanded rows works the same way as pinning regular rows. You can pin expanded rows to the top or bottom of the table. Please refer to the [Row Pinning Guide](./row-pinning) for more information on row pinning. + +### Sorting Expanded Rows + +By default, expanded rows are sorted along with the rest of the table. + +### Auto Reset Expanded State + +If you are also using the grouping feature, the `expanded` state is automatically reset whenever the grouped row model recomputes, such as when the `data` or the grouping state changes. This default is automatically disabled when `manualExpanding` is `true`, but it can be overridden by explicitly assigning a boolean value to the `autoResetExpanded` table option. There is also a global `autoResetAll` table option that disables (or enables) every auto-reset behavior at once. + +A common reason to set `autoResetExpanded: false` is editing data while viewing the table (for example, inline cell editing). Every edit updates `data`, which recomputes the row models and would otherwise collapse the user's expanded rows. If you also use the pagination feature, pair it with `autoResetPageIndex: false` so the current page is kept as well. + +```ts +const table = useTable({ + features, + // other options... + autoResetExpanded: false, // keep expanded state when data changes + // autoResetAll: false, // or turn off all auto resets at once +}) +``` + +### Manual Expanding (server-side) + +If you are doing server-side expansion, you can enable manual row expansion by setting the manualExpanding option to true. This means that the `getExpandedRowModel` will not be used to expand rows and you would be expected to perform the expansion in your own data model. + +```ts +const features = tableFeatures({ rowExpandingFeature }) // no expandedRowModel for manual expanding + +const table = useTable({ + features, + // other options... + manualExpanding: true, +}) +``` diff --git a/docs/framework/octane/guide/fuzzy-filtering.md b/docs/framework/octane/guide/fuzzy-filtering.md new file mode 100644 index 0000000000..f0ef16b88d --- /dev/null +++ b/docs/framework/octane/guide/fuzzy-filtering.md @@ -0,0 +1,219 @@ +--- +title: Fuzzy Filtering (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Fuzzy Search](../examples/filters-fuzzy) + +### Fuzzy Filtering Setup + +Here's how you set up your table to use fuzzy filtering features. Adding the fuzzy filtering feature enables the related APIs. Additionally, if using client-side fuzzy filtering and sorting, you also need to set up `filteredRowModel` and `sortedRowModel` after their associated features because row model slots are type-checked. + +```tsx +import { + useTable, + tableFeatures, + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + createFilteredRowModel, + createSortedRowModel, + metaHelper, +} from '@tanstack/octane-table' +import type { RankingInfo } from '@tanstack/match-sorter-utils' + +interface FuzzyFilterMeta { + itemRank?: RankingInfo +} + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + sortedRowModel: createSortedRowModel(), // if using client-side sorting + filterFns: { fuzzy: fuzzyFilter }, + sortFns: { fuzzy: fuzzySort }, + filterMeta: metaHelper(), +}) + +const table = useTable({ + features, + columns, + data, +}) +``` + +> **Note:** The `filterFns` and `sortFns` registries above list only the custom `fuzzy` functions this guide uses. Spreading the entire built-in registries (`filterFns: { ...filterFns, fuzzy: fuzzyFilter }`) still works, but it puts every built-in function in your bundle. Register just the functions you use, or pass functions directly to the `filterFn` and `sortFn` column options with no registration. + +## Fuzzy Filtering (Octane) 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 type { RankingInfo } from '@tanstack/match-sorter-utils' +import type { + FilterFn, + TableFeatures, + RowData, + metaHelper, +} from '@tanstack/octane-table' + +// Define the shape of the filter meta stored by the fuzzy filter +interface FuzzyFilterMeta { + itemRank?: RankingInfo +} + +// Extend TableFeatures so the FilterFn type has access to the filterMeta shape +type FuzzyFeatures = TableFeatures & { filterMeta: FuzzyFilterMeta } + +const fuzzyFilter: FilterFn = ( + row, + columnId, + value, + addMeta, +) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value) + + // Store the itemRank info + addMeta?.({ itemRank }) + + // Return if the item should be filtered in/out + return itemRank.passed +} +``` + +In this function, we're using the `rankItem` function from the `@tanstack/match-sorter-utils` library to rank the item. We then store the ranking information in the filter meta of the row (the `addMeta` callback is optional, so call it with optional chaining), and return whether the item passed the ranking criteria. + +Instead of using `declare module` augmentation, register the fuzzy filter and its meta shape as slots on `tableFeatures`: + +```typescript +import { metaHelper } from '@tanstack/octane-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + filteredRowModel: createFilteredRowModel(), + sortedRowModel: createSortedRowModel(), + filterFns: { fuzzy: fuzzyFilter }, + sortFns: { fuzzy: fuzzySort }, + filterMeta: metaHelper(), +}) +``` + +The `filterMeta` slot types the metadata stored by `addMeta`. The `filterFns` slot registers the function so it can be referenced by the string name `'fuzzy'` in column definitions and `globalFilterFn`. + +### Using Fuzzy Filtering with Global Filtering + +To use fuzzy filtering with global filtering, register the fuzzy filter function in the `filterFns` slot on `tableFeatures` and reference it in the `globalFilterFn` option of the table: + +```typescript +import { + useTable, + tableFeatures, + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + createFilteredRowModel, + createSortedRowModel, + metaHelper, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + filteredRowModel: createFilteredRowModel(), + sortedRowModel: createSortedRowModel(), // needed if you want sorting with fuzzy rank + filterFns: { fuzzy: fuzzyFilter }, + sortFns: { fuzzy: fuzzySort }, + filterMeta: metaHelper(), +}) + +const table = useTable({ + features, + columns, + data, + globalFilterFn: 'fuzzy', +}) +``` + +### Using Fuzzy Filtering with Column Filtering + +To use fuzzy filtering with column filtering, register your fuzzy filter function in the `filterFns` slot on `tableFeatures` (as shown above). You can then specify the fuzzy filter by name in the `filterFn` option of the column definition: + +```typescript +const column = [ + { + accessorFn: (row) => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: (info) => info.getValue(), + filterFn: 'fuzzy', //using our custom fuzzy filter function + }, + // other columns... +] +``` + +In this example, we're applying the fuzzy filter to a column that combines the firstName and lastName fields of the data. + +#### Sorting with Fuzzy Filtering + +When using fuzzy filtering with column filtering, you might also want to sort the data based on the ranking information. You can do this by defining a custom sorting function: + +```typescript +import { compareItems } from '@tanstack/match-sorter-utils' +import { sortFn_alphanumeric } from '@tanstack/octane-table' +import type { SortFn } from '@tanstack/octane-table' + +const fuzzySort: SortFn = (rowA, rowB, columnId) => { + let dir = 0 + + // Only sort by rank if the column has ranking information + if (rowA.columnFiltersMeta[columnId]) { + dir = compareItems( + rowA.columnFiltersMeta[columnId].itemRank!, + rowB.columnFiltersMeta[columnId].itemRank!, + ) + } + + // Provide an alphanumeric fallback for when the item ranks are equal + return dir === 0 ? sortFn_alphanumeric(rowA, rowB, columnId) : dir +} +``` + +In this function, we're comparing the ranking information of the two rows. If the ranks are equal, we fall back to alphanumeric sorting. + +You can then pass this sorting function directly to the `sortFn` option of the column definition: + +```typescript +{ + accessorFn: row => `${row.firstName} ${row.lastName}`, + id: 'fullName', + header: 'Full Name', + cell: info => info.getValue(), + filterFn: 'fuzzy', // using our custom fuzzy filter function (registered above) + sortFn: fuzzySort, // pass our custom fuzzy sort function directly +} +``` + +> **Note:** Unlike `filterFn: 'fuzzy'` above, `fuzzySort` is passed as a function rather than a string. A string reference like `sortFn: 'fuzzySort'` would require you to also add it to the `sortFns` slot on `tableFeatures` (e.g. `sortFns: { fuzzySort }`). Passing the function directly skips registration. diff --git a/docs/framework/octane/guide/global-filtering.md b/docs/framework/octane/guide/global-filtering.md new file mode 100644 index 0000000000..62972d7e5d --- /dev/null +++ b/docs/framework/octane/guide/global-filtering.md @@ -0,0 +1,274 @@ +--- +title: Global Filtering (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Column Filters](../examples/filters) +- [Fuzzy Search](../examples/filters-fuzzy) + +### Global Filtering Setup + +Here's how you set up your table to use global filtering features. Global filtering depends on column filtering, so add `columnFilteringFeature` before `globalFilteringFeature`. Adding the global filtering feature enables the related APIs. Additionally, if using client-side filtering, you also need to set up `filteredRowModel` after its associated feature because row model slots are type-checked. + +```tsx +import { + useTable, + tableFeatures, + columnFilteringFeature, + globalFilteringFeature, + createFilteredRowModel, + filterFn_includesString, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + filteredRowModel: createFilteredRowModel(), // if using client-side filtering + filterFns: { includesString: filterFn_includesString }, +}) + +const table = useTable({ + features, + columns, + data, +}) +``` + +> **Note:** The `filterFns` registry above lists only the built-in filter function this table uses. Spreading the entire built-in `filterFns` registry (`filterFns: { ...filterFns }`) still works, but it puts every built-in filter function in your bundle. Register just the functions you use, or pass a function directly to the `globalFilterFn` option with no registration at all. + +## Global Filtering (Octane) 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 `tableFeatures`, you can tell the table to skip it by setting the `manualFiltering` option to `true`. + +```tsx +import { + useTable, + tableFeatures, + columnFilteringFeature, + globalFilteringFeature, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, +}) // no filteredRowModel for manual server-side global filtering + +const table = useTable({ + features, + data, + columns, + manualFiltering: true, +}) +``` + +Note: When using manual global filtering, many of the options that are discussed in the rest of this guide will have no effect. When manualFiltering is set to true, the table instance will not apply any global filtering logic to the rows that are passed to it. Instead, it will assume that the rows are already filtered and will use the data that you pass to it as-is. + +### Client-Side Global Filtering + +If you are using the built-in client-side global filtering, add the `globalFilteringFeature` (along with its required `columnFilteringFeature` prerequisite), the `filteredRowModel` factory, and `filterFns` to your `tableFeatures` call: + +```tsx +import { + useTable, + tableFeatures, + columnFilteringFeature, + globalFilteringFeature, + createFilteredRowModel, + filterFn_includesString, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + filteredRowModel: createFilteredRowModel(), + filterFns: { includesString: filterFn_includesString }, +}) + +const table = useTable({ + features, + // 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 filter function (built-in or custom) registered in the `filterFns` slot on `tableFeatures`, or a filter function passed directly. + +```tsx +const table = useTable({ + features, + data, + columns, + globalFilterFn: 'includesString', // built-in filter function +}) +``` + +By default there are 12 built-in filter functions to choose from: + +- `includesString` - Case-insensitive string inclusion +- `includesStringSensitive` - Case-sensitive string inclusion +- `equalsString` - Case-insensitive string equality +- `equals` - Strict equality `===` +- `weakEquals` - Weak equality `==` +- `arrIncludes` - The row's array (or string) value includes at least one of the filter values +- `arrIncludesAll` - The row's array value includes every filter value +- `arrIncludesSome` - The row's array value includes at least one of the filter values +- `arrHas` - The row's scalar value equals at least one of the filter values +- `inNumberRange` - Inclusive `[min, max]` number range (endpoints normalized and swapped if reversed) +- `between` - Exclusive min/max range (blank endpoints are open-ended) +- `betweenInclusive` - Inclusive min/max range (blank endpoints are open-ended) + +You can also define your own custom global filter function and pass it directly to the `globalFilterFn` table option, as shown [below](#custom-global-filter-function). + +### Global Filter State + +The `globalFilter` state slice holds the current global filter value, usually a search string (the slice is typed as `any` so custom global filter functions can accept other value shapes). For reactive reads that should re-render your UI, use `table.state.globalFilter`. In event handlers, you can read the current snapshot with `table.atoms.globalFilter.get()`, but this read does not subscribe the component to future changes. + +If you need access to the global filter state outside of the table, you can own the slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. Atoms preserve fine-grained subscriptions, and the filter value can be used elsewhere (such as in a query key for server-side filtering) without forcing the component that owns the table to re-render. + +```tsx +import { useCreateAtom, useSelector } from '@tanstack/octane-store' + +const globalFilterAtom = useCreateAtom('') + +// subscribe to the atom wherever you need the value (e.g. for a query key) +const globalFilter = useSelector(globalFilterAtom) + +const table = useTable({ + features, + // other options... + atoms: { + globalFilter: globalFilterAtom, // table.setGlobalFilter now updates globalFilterAtom + }, +}) +``` + +Alternatively, the v8-style `state.globalFilter` plus `onGlobalFilterChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```tsx +const [globalFilter, setGlobalFilter] = useState('') + +const table = useTable({ + features, + // other options... + state: { + globalFilter, + }, + onGlobalFilterChange: setGlobalFilter, +}) +``` + +### Adding global filter input to UI + +TanStack table will not add a global filter input UI to your table. You should manually add it to your UI to allow users to filter the table. For example, you can add an input UI above the table to allow users to enter a search term. Read the value reactively with `table.state.globalFilter` and update it with `table.setGlobalFilter`. + +```tsx +return ( +
+ + table.setGlobalFilter(String((e.target as HTMLInputElement).value)) + } + placeholder="Search..." + /> +
+) +``` + +> **Note:** Use `onInput` for text inputs in Octane. `onChange` is the native change event and only fires on blur or Enter, not on every keystroke. + +### 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). + +```tsx +const customFilterFn = (row, columnId, filterValue) => { + return // true if the row should be included in the filtered rows +} + +const table = useTable({ + features, + // other options... + globalFilterFn: customFilterFn, +}) +``` + +### Initial Global Filter State + +If you want to set an initial global filter state when the table is initialized, you can pass the global filter state as part of the table `initialState` option. However, if you are controlling the slice yourself, set the starting value on your external atom or Octane state instead. + +```tsx +const table = useTable({ + features, + // other options... + initialState: { + globalFilter: 'search term', // if not controlling globalFilter state, set initial state here + }, +}) +``` + +> NOTE: Do not use both `initialState.globalFilter` and a controlled `globalFilter` (via `atoms` or `state`) at the same time, as the controlled value will override `initialState.globalFilter`. + +### Disable Global Filtering + +By default, global filtering is enabled for all columns. You can disable the global filtering for all columns by using the enableGlobalFilter table option. You can also turn off both column and global filtering by setting the enableFilters table option to false. + +Disabling global filtering will cause the column.getCanGlobalFilter API to return false for that column. + +```tsx +const columns = [ + { + header: () => 'Id', + accessorKey: 'id', + enableGlobalFilter: false, // disable global filtering for this column + }, + //... +] +//... +const table = useTable({ + features, + // other options... + columns, + enableGlobalFilter: false, // disable global filtering for all columns +}) +``` + +### Global Filter APIs + +There are several APIs that are useful for hooking up your global filter UI: + +- `table.setGlobalFilter` - Set the global filter value. Useful for connecting a search input's `onInput` handler. +- `table.resetGlobalFilter` - Reset the global filter value to its initial state, or clear it with `table.resetGlobalFilter(true)`. +- `table.getGlobalFilterFn` - Returns the filter function currently used for global filtering. +- `table.getGlobalAutoFilterFn` - Returns the default global filter function (currently `includesString`). +- `column.getCanGlobalFilter` - Returns whether a column participates in global filtering. Useful for debugging which columns are searched. diff --git a/docs/framework/octane/guide/grouping.md b/docs/framework/octane/guide/grouping.md new file mode 100644 index 0000000000..cb1b30ab92 --- /dev/null +++ b/docs/framework/octane/guide/grouping.md @@ -0,0 +1,197 @@ +--- +title: Grouping (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Grouping](../examples/grouping) + +> **Note:** `columnGroupingFeature` and `rowAggregationFeature` are now separate features. Register either one independently, or register both when grouped rows should also calculate aggregate values. See the [Aggregation Guide](./aggregation) for aggregation setup. + +### Grouping Setup + +Here's how you set up your table to use grouping features. Adding the grouping feature enables the related APIs. Additionally, if using client-side grouping, you also need to set up `groupedRowModel` after its associated feature because row model slots are type-checked. + +```tsx +import { + useTable, + tableFeatures, + columnGroupingFeature, + createGroupedRowModel, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + columnGroupingFeature, + groupedRowModel: createGroupedRowModel(), // if using client-side grouping +}) + +const table = useTable({ + features, + columns, + data, +}) +``` + +## Grouping (Octane) Guide + +Grouping in TanStack table is a feature that applies to columns and allows you to categorize and organize the table rows based on specific columns. This can be useful in cases where you have a large amount of data and you want to group them together based on certain criteria. + +Grouping can also affect column order. There are 3 table features that can reorder columns, which happen in the following order: + +1. [Column Pinning](./column-pinning) - If pinning, columns are split into start, center (unpinned), and end pinned columns. +2. Manual [Column Ordering](./column-ordering) - A manually specified column order is applied. +3. **Grouping** - If grouping is enabled, a grouping state is active, and `tableOptions.groupedColumnMode` is set to `'reorder' | 'remove'`, then the grouped columns are reordered to the start of the column flow. + +To group rows, add `columnGroupingFeature` and the `groupedRowModel` factory. + +```tsx +import { + useTable, + tableFeatures, + columnGroupingFeature, + createGroupedRowModel, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + columnGroupingFeature, + groupedRowModel: createGroupedRowModel(), +}) + +const table = useTable({ + features, + // other options... +}) +``` + +When grouping state is active, the table will add matching rows as subRows to the grouped row. The grouped row will be added to the table rows at the same index as the first matching row. The matching rows will be removed from the table rows. +To allow the user to expand and collapse the grouped rows, you can use the expanding feature. + +```tsx +const features = tableFeatures({ + columnGroupingFeature, + rowExpandingFeature, + groupedRowModel: createGroupedRowModel(), + expandedRowModel: createExpandedRowModel(), +}) + +const table = useTable({ + features, + // other options... +}) +``` + +### Grouping state + +The grouping state is an array of strings, where each string is the ID of a column to group by. The order of the strings in the array determines the order of the grouping. For example, if the grouping state is ['column1', 'column2'], then the table will first group by column1, and then within each group, it will group by column2. You can control the grouping state using the setGrouping function: + +```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, + // other options... + groupedColumnMode: 'reorder', +}) +``` + +### Manual Grouping + +If you are doing server-side grouping, you can enable manual grouping using the manualGrouping option. When this option is set to true, the table will not automatically group rows using getGroupedRowModel() and instead will expect you to group the rows before passing them to the table. + +```tsx +const features = tableFeatures({ columnGroupingFeature }) // no groupedRowModel for manual grouping + +const table = useTable({ + features, + // other options... + manualGrouping: true, +}) +``` + +> **Note:** There are not currently many known easy ways to do server-side grouping with TanStack Table. You will need to do lots of custom cell rendering to make this work. + +### Controlled Grouping State + +If you need access to the grouping state in other parts of your application, you can own the `grouping` state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. Atoms preserve fine-grained subscriptions, and the grouping value can be read anywhere in your app (such as in a query key for server-side grouping) without forcing the component that owns the table to re-render. + +```tsx +import { useCreateAtom, useSelector } from '@tanstack/octane-store' +import type { GroupingState } from '@tanstack/octane-table' + +const groupingAtom = useCreateAtom([]) + +// subscribe to the atom wherever you need the value +const grouping = useSelector(groupingAtom) + +const table = useTable({ + features, + // other options... + atoms: { + grouping: groupingAtom, // grouping APIs now update groupingAtom + }, +}) +``` + +Alternatively, the v8-style `state.grouping` plus `onGroupingChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```tsx +const [grouping, setGrouping] = useState([]) + +const table = useTable({ + features, + // 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() +``` + +Rows expose grouping helpers for grouped row rendering: + +```tsx +row.getIsGrouped() +row.getGroupingValue(columnId) +row.groupingColumnId +row.groupingValue +``` + +Cells expose grouping and placeholder helpers: + +```tsx +cell.getIsGrouped() +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/octane/guide/pagination.md b/docs/framework/octane/guide/pagination.md new file mode 100644 index 0000000000..950a3d9f89 --- /dev/null +++ b/docs/framework/octane/guide/pagination.md @@ -0,0 +1,284 @@ +--- +title: Pagination (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Pagination](../examples/pagination) + +### Pagination Setup + +Here's how you set up your table to use pagination features. Adding the pagination feature enables the related APIs. Additionally, if using client-side pagination, you also need to set up `paginatedRowModel` after its associated feature because row model slots are type-checked. + +```tsx +import { + useTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), // if using client-side pagination +}) + +const table = useTable({ + features, + columns, + data, +}) +``` + +## Pagination (Octane) 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 1,000 rows by default and includes a 200,000 row stress-test button that still performs well, albeit with only a 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` and the `paginatedRowModel` factory to your `tableFeatures` call: + +```tsx +import { + useTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), +}) + +const table = useTable({ + features, + columns, + data, +}) +``` + +### Manual Server-Side Pagination + +If you decide that you need to use server-side pagination, here is how you can implement it. + +No pagination row model is needed for server-side pagination, but if you have provided it for other tables that do need it in a shared component, you can still turn off the client-side pagination by setting the `manualPagination` option to `true`. Setting the `manualPagination` option to `true` will tell the table instance to use the `table.getPrePaginatedRowModel` row model under the hood, and it will make the table instance assume that the `data` that you pass in is already paginated. + +#### Page Count and Row Count + +The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, 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/octane-table' + +const features = tableFeatures({ rowPaginationFeature }) // no paginatedRowModel for server-side pagination + +const table = useTable({ + features, + columns, + data, + manualPagination: true, // turn off client-side pagination + rowCount: dataQuery.data?.rowCount, // pass in the total row count so the table knows how many pages there are (pageCount calculated internally if not provided) + // pageCount: dataQuery.data?.pageCount, // alternatively directly pass in pageCount instead of rowCount +}) +``` + +> **Note**: Setting the `manualPagination` option to `true` will make the table instance assume that the `data` that you pass in is already paginated. + +### Pagination State + +Whether or not you are using client-side or manual server-side pagination, you can use the built-in `pagination` state and APIs. + +The `pagination` state is an object that contains the following properties: + +- `pageIndex`: The current page index (zero-based). +- `pageSize`: The current page size. + +For reactive reads that should re-render your UI, use `table.state.pagination`. In event handlers, you can read the current snapshot with `table.atoms.pagination.get()`, but this read does not subscribe the component to future changes. + +If you need access to the `pagination` state outside of the table (a server-side query key is the most common case), you can own the slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. Atoms preserve fine-grained subscriptions, and the pagination value can be used in a query key without forcing the component that owns the table to re-render. + +```tsx +import { useCreateAtom, useSelector } from '@tanstack/octane-store' +import { + useTable, + tableFeatures, + rowPaginationFeature, + createPaginatedRowModel, + type PaginationState, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), +}) + +const paginationAtom = useCreateAtom({ + pageIndex: 0, // initial page index + pageSize: 10, // default page size +}) + +// subscribe to the atom wherever you need the value (e.g. for a query key) +const pagination = useSelector(paginationAtom) + +const table = useTable({ + features, + columns, + data, + atoms: { + pagination: paginationAtom, // table pagination APIs now update paginationAtom + }, +}) +``` + +Alternatively, the v8-style `state.pagination` plus `onPaginationChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```tsx +const [pagination, setPagination] = useState({ + pageIndex: 0, // initial page index + pageSize: 10, // default page size +}) + +const table = useTable({ + features, + 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, + columns, + data, + initialState: { + pagination: { + pageIndex: 2, // custom initial page index + pageSize: 25, // custom default page size + }, + }, +}) +``` + +> **Note**: Do NOT provide the `pagination` slice in more than one of the `atoms`, `state`, and `initialState` options. Controlled values (`atoms` or `state`) will overwrite `initialState`. Only use one of them. + +### Pagination Options + +Besides the `manualPagination`, `pageCount`, and `rowCount` options which are useful for manual server-side pagination (and discussed [above](#manual-server-side-pagination)), there is one other table option that is useful to understand. + +#### Auto Reset Page Index + +By default, `pageIndex` is reset to `0` whenever the client-side row models recompute, such as when the `data` is updated, filters change, sorting changes, or grouping changes. This behavior is automatically disabled when `manualPagination` is `true`, but it can be overridden by explicitly assigning a boolean value to the `autoResetPageIndex` table option. There is also a global `autoResetAll` table option that disables (or enables) every auto-reset behavior at once. + +```tsx +const table = useTable({ + features, + columns, + data, + autoResetPageIndex: false, // turn off auto reset of pageIndex + // autoResetAll: false, // or turn off all auto resets at once +}) +``` + +A common reason to set `autoResetPageIndex: false` is editing data while viewing the table (for example, inline cell editing). Every edit updates `data`, which recomputes the row models and would otherwise snap the user back to the first page. Setting the option to a static `false` keeps the current page when the row model recomputes. If you also use the expanding feature, pair it with `autoResetExpanded: false` so expanded rows do not collapse on edits. + +Be aware, however, that if you turn off `autoResetPageIndex`, you may need to add some logic to handle resetting the `pageIndex` yourself to avoid showing empty pages. + +### Pagination APIs + +There are several pagination table instance APIs that are useful for hooking up your pagination UI components. + +#### Pagination Button APIs + +- `getCanPreviousPage`: Useful for disabling the "previous page" button when on the first page. +- `getCanNextPage`: Useful for disabling the "next page" button when there are no more pages. +- `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/octane/guide/row-pinning.md b/docs/framework/octane/guide/row-pinning.md new file mode 100644 index 0000000000..34231b74f2 --- /dev/null +++ b/docs/framework/octane/guide/row-pinning.md @@ -0,0 +1,249 @@ +--- +title: Row Pinning (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Row Pinning](../examples/row-pinning) + +### Row Pinning Setup + +Here's how you set up your table to use row pinning features. Adding the row pinning feature enables the related APIs. + +```tsx +import { + useTable, + tableFeatures, + rowPinningFeature, +} from '@tanstack/octane-table' + +const features = tableFeatures({ rowPinningFeature }) + +const table = useTable({ + features, + columns, + data, +}) +``` + +## Row Pinning (Octane) 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; add other row model factory slots to `tableFeatures` only if your table uses them. + +```tsx +import { + rowPinningFeature, + tableFeatures, + useTable, +} from '@tanstack/octane-table' + +const features = tableFeatures({ rowPinningFeature }) + +const table = useTable({ + features, + 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, + columns, + data, + initialState: { + rowPinning: { + top: ['0'], + bottom: ['3'], + }, + }, +}) +``` + +If you need to manage row pinning outside of the table instance, the recommended v9 approach is an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the pinning state without re-rendering the component that owns the table. + +```tsx +import { useCreateAtom, useSelector } from '@tanstack/octane-store' +import type { RowPinningState } from '@tanstack/octane-table' + +const rowPinningAtom = useCreateAtom({ + top: [], + bottom: [], +}) + +const rowPinning = useSelector(rowPinningAtom) // subscribe wherever it is needed + +const table = useTable({ + features, + columns, + data, + atoms: { + rowPinning: rowPinningAtom, + }, +}) +``` + +Alternatively, the v8-style `state.rowPinning` plus `onRowPinningChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```tsx +const [rowPinning, setRowPinning] = useState({ + top: [], + bottom: [], +}) + +const table = useTable({ + features, + 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, + 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, + columns, + data, + keepPinnedRows: false, +}) +``` diff --git a/docs/framework/octane/guide/row-selection.md b/docs/framework/octane/guide/row-selection.md new file mode 100644 index 0000000000..df29933d9d --- /dev/null +++ b/docs/framework/octane/guide/row-selection.md @@ -0,0 +1,258 @@ +--- +title: Row Selection (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Row Selection](../examples/row-selection) + +### Row Selection Setup + +Here's how you set up your table to use row selection features. Adding the row selection feature enables the related APIs. + +```tsx +import { + useTable, + tableFeatures, + rowSelectionFeature, +} from '@tanstack/octane-table' + +const features = tableFeatures({ rowSelectionFeature }) + +const table = useTable({ + features, + columns, + data, +}) +``` + +## Row Selection (Octane) Guide + +The row selection feature keeps track of which rows are selected and allows you to toggle the selection of rows in a myriad of ways. Let's take a look at some common use cases. + +### Access Row Selection State + +The table instance already manages the row selection state for you. You can access the row selection state or the selected rows from a few APIs. + +- `table.state.rowSelection` - returns the row selection state reactively (selected by the `useTable` selector) +- `getSelectedRowModel()` - returns selected rows +- `getFilteredSelectedRowModel()` - returns selected rows after filtering +- `getGroupedSelectedRowModel()` - returns selected rows after grouping and sorting + +```ts +console.log(table.state.rowSelection) //get the row selection state - { 1: true, 2: false, etc... } +console.log(table.getSelectedRowModel().rows) //get full client-side selected rows +console.log(table.getFilteredSelectedRowModel().rows) //get filtered client-side selected rows +console.log(table.getGroupedSelectedRowModel().rows) //get grouped client-side selected rows +``` + +In event handlers or other non-render code, you can also read the current snapshot with `table.atoms.rowSelection.get()`. This read does not subscribe a component to future changes, so prefer `table.state.rowSelection` (or `table.Subscribe`) in render positions. + +> Note: If you are using `manualPagination`, be aware that the `getSelectedRowModel` API will only return selected rows on the current page because table row models can only generate rows based on the `data` that is passed in. Row selection state, however, can contain row ids that are not present in the `data` array just fine. + +### Manage Row Selection State + +If you need easy access to the selected row ids in other parts of your application (for example, to make API calls with them), you can own the row selection state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. Atoms preserve fine-grained subscriptions, and the selection value can be read anywhere in your app without forcing the component that owns the table to re-render. + +```ts +import { useCreateAtom, useSelector } from '@tanstack/octane-store' +import { + useTable, + tableFeatures, + rowSelectionFeature, + type RowSelectionState, +} from '@tanstack/octane-table' + +const features = tableFeatures({ rowSelectionFeature }) + +const rowSelectionAtom = useCreateAtom({}) + +// subscribe to the atom wherever you need the value +const rowSelection = useSelector(rowSelectionAtom) + +const table = useTable({ + features, + //... + atoms: { + rowSelection: rowSelectionAtom, // selection APIs now update rowSelectionAtom + }, +}) +``` + +Alternatively, the v8-style `state.rowSelection` plus `onRowSelectionChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```ts +const [rowSelection, setRowSelection] = useState({}) + +const table = useTable({ + features, + //... + 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, + //... + 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 +}) +``` + +### Shift Range Selection + +`row.getToggleSelectedHandler()` supports Shift range selection by default. After an ordinary selectable-row interaction establishes an anchor, Shift-selecting another row selects or deselects the inclusive interval between them. The clicked checkbox's resulting checked value controls the whole range, and the clicked endpoint becomes the anchor for the next Shift interaction. + +The handler recognizes Shift when the event exposes either `event.shiftKey` or `event.nativeEvent.shiftKey`. You can disable range behavior or replace event detection: + +Bind a Octane checkbox handler with `onClick`, not `onChange`, so the handler receives the click event and its `shiftKey` modifier. + +```ts +const table = useTable({ + // ... + enableRowRangeSelection: false, + + // For example, use the platform modifier instead of Shift: + // isRowRangeSelectionEvent: event => + // Boolean((event as { metaKey?: boolean }).metaKey), +}) +``` + +Range selection follows the table's current logical display order, including filtering, grouping, sorting, and expansion. With client-side pagination, ranges can cross pages because the complete pre-pagination order is used. With manual/server pagination, only rows loaded in the current `data` can participate. + +By default, a parent encountered in a range recursively toggles its selectable descendants when sub-row selection is enabled. Pass `selectChildren: false` when only rows explicitly present in the display-order interval should change: + +```ts +const handler = row.getToggleSelectedHandler({ + selectChildren: false, +}) +``` + +The interaction anchor is preserved across sorting, filtering, grouping, expansion, pagination, and data updates while its row id remains in the display order. If filtering or data replacement removes the anchor, the next Shift interaction falls back to an ordinary row toggle and establishes a new anchor. `resetRowSelection`, either select-all API, and `table.reset()` clear the anchor. Direct calls to `row.toggleSelected()` or `table.setRowSelection()`, and external controlled-state changes, do not establish or move it. + +### Render Row Selection UI + +TanStack table does not dictate how you should render your row selection UI. You can use checkboxes, radio buttons, or simply hook up click events to the row itself. The table instance provides a few APIs to help you render your row selection UI. + +#### Connect Row Selection APIs to Checkbox Inputs + +TanStack Table provides some handler functions that you can connect directly to your checkbox inputs to make it easy to toggle row selection. These 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/octane/guide/sorting.md b/docs/framework/octane/guide/sorting.md new file mode 100644 index 0000000000..db747e9ed3 --- /dev/null +++ b/docs/framework/octane/guide/sorting.md @@ -0,0 +1,559 @@ +--- +title: Sorting (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Sorting](../examples/sorting) + +### Sorting Setup + +Here's how you set up your table to use sorting features. Adding the sorting feature enables the related APIs. Additionally, if using client-side sorting, you also need to set up `sortedRowModel` after its associated feature because row model slots are type-checked. + +```tsx +import { + useTable, + tableFeatures, + rowSortingFeature, + createSortedRowModel, + sortFn_alphanumeric, + sortFn_text, + sortFn_datetime, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), // if using client-side sorting + sortFns: { + alphanumeric: sortFn_alphanumeric, + text: sortFn_text, + datetime: sortFn_datetime, + }, +}) + +const table = useTable({ + features, + columns, + data, +}) +``` + +> **NOTE**: Spreading the entire built-in registry (`sortFns: { ...sortFns }`) still works, but it puts every built-in sorting function in your bundle. Registering just the functions you use, or passing a function directly to the `sortFn` column option, is recommended. The default `sortFn: 'auto'` resolves to `alphanumeric`, `text`, or `datetime` from the registry based on the column's data type, so register the ones your columns rely on. + +## Sorting (Octane) 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 + +For reactive reads that should re-render your UI, use `table.state.sorting` (selected by the `useTable` selector) or `table.Subscribe`. In event handlers or other non-render code, you can read the current snapshot with `table.atoms.sorting.get()`, but be aware that this read does not subscribe the component to future changes. + +```tsx +const table = useTable({ + features, + columns, + data, + //... +}) + +table.state.sorting // reactive read in render +table.atoms.sorting.get() // snapshot read in event handlers +``` + +However, if you need access to the sorting state outside of the table, you can "control" the sorting state like down below. + +#### Controlled Sorting State + +If you need easy access to the sorting state in other parts of your application, you can own the sorting state slice yourself. The recommended way in v9 is an external atom passed through the `atoms` table option. Atoms preserve fine-grained subscriptions, and the sorting value can be used elsewhere (such as in a query key for server-side sorting) without forcing the component that owns the table to re-render. + +```tsx +import { useCreateAtom, useSelector } from '@tanstack/octane-store' + +const sortingAtom = useCreateAtom([]) // can set initial sorting state here + +// subscribe to the atom wherever you need the value (e.g. for a query key) +const sorting = useSelector(sortingAtom) + +const table = useTable({ + features, + columns, + data, + //... + atoms: { + sorting: sortingAtom, // table sorting APIs now update sortingAtom + }, +}) +``` + +Alternatively, the v8-style `state.sorting` plus `onSortingChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```tsx +const [sorting, setSorting] = useState([]) +//... +const table = useTable({ + features, + 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, + 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 +import { useCreateAtom, useSelector } from '@tanstack/octane-store' + +const features = tableFeatures({ rowSortingFeature }) // feature needed for sorting state/APIs; no sortedRowModel for manual sorting + +const sortingAtom = useCreateAtom([]) + +// subscribe to the sorting state for your server-side query +const sorting = useSelector(sortingAtom) +//... +const table = useTable({ + features, + columns, + data, + manualSorting: true, // use pre-sorted row model instead of sorted row model + atoms: { + sorting: sortingAtom, + }, +}) +``` + +Hoisting the sorting state into your own scope (with an external atom or the `state.sorting` plus `onSortingChange` pattern) is covered in the [Controlled Sorting State](#controlled-sorting-state) section above. + +> **NOTE**: When `manualSorting` is set to `true`, the table will assume that the data that you provide is already sorted, and will not apply any sorting to it. + +### Client-Side Sorting + +To implement client-side sorting, add the `rowSortingFeature` and the `sortedRowModel` factory to your `tableFeatures` call. Import `createSortedRowModel` and the individual sorting functions you use from TanStack Table: + +```tsx +import { + useTable, + tableFeatures, + rowSortingFeature, + createSortedRowModel, + sortFn_alphanumeric, + sortFn_text, + sortFn_datetime, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns: { + alphanumeric: sortFn_alphanumeric, + text: sortFn_text, + datetime: sortFn_datetime, + }, +}) + +const table = useTable({ + features, + columns, + data, +}) +``` + +### Sorting RowModelFns + +The default sorting function for all columns is inferred from the data type of the column. However, it can be useful to define the exact sorting function that you want to use for a specific column, especially if any of your data is nullable or not a standard data type. + +You can determine a custom sorting function on a per-column basis using the `sortFn` column option. + +By default, there are 6 built-in sorting functions to choose from: + +- `alphanumeric` - Sorts by mixed alphanumeric values without case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `alphanumericCaseSensitive` - Sorts by mixed alphanumeric values with case-sensitivity. Slower, but more accurate if your strings contain numbers that need to be naturally sorted. +- `text` - Sorts by text/string values without case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `textCaseSensitive` - Sorts by text/string values with case-sensitivity. Faster, but less accurate if your strings contain numbers that need to be naturally sorted. +- `datetime` - Sorts by time, use this if your values are `Date` objects. +- `basic` - Sorts using a basic/standard `a > b ? 1 : a < b ? -1 : 0` comparison. This is the fastest sorting function, but may not be the most accurate. + +You can also define your own custom sorting functions, either inline as the `sortFn` column option, or by name in the sorting function registry that you pass to `createSortedRowModel`. + +#### Custom Sorting Functions + +Whether you register a custom sorting function in the registry passed to `createSortedRowModel` or pass it directly as a `sortFn` column option, it should have the following signature: + +```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 care 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', // reference a custom sorting function registered with createSortedRowModel + }, + { + header: () => 'Birthday', + accessorKey: 'birthday', + sortFn: 'datetime', // recommended for date columns + }, + { + header: () => 'Profile', + accessorKey: 'profile', + // use custom sorting function directly + sortFn: (rowA, rowB, columnId) => { + return rowA.original.someProperty - rowB.original.someProperty + }, + }, +] +//... +const myCustomSortFn: SortFn = ( + rowA, + rowB, + columnId, +) => + rowA.original[columnId] > rowB.original[columnId] + ? 1 + : rowA.original[columnId] < rowB.original[columnId] + ? -1 + : 0 + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns: { + alphanumeric: sortFn_alphanumeric, + datetime: sortFn_datetime, + myCustomSortFn, + }, +}) + +const table = useTable({ + features, + columns, + data, +}) +``` + +> **TypeScript Note:** For `sortFn: 'myCustomSortFn'` string references to typecheck, register the function in the `sortFns` slot on `tableFeatures` (as shown above). TypeScript will infer the registered names from the slot automatically. Alternatively, skip the registry entirely by passing the function directly to the `sortFn` column option. + +#### Customize Sorting Function Behavior + +Sorting functions support an optional "hanging" property: + +- `sortFn.resolveDataValue` - normalizes each row's value before the two sides are compared. It is honored by every sorting function built with the `constructSortFn` helper, which includes all built-in sorting functions. + +The `constructSortFn` helper builds a sorting function from a value-level comparator (`sort`) plus that optional resolver. Keeping the comparison in `sort` and the normalization in `resolveDataValue` means a variant of an existing sorting function only has to swap the resolver: the definition is attached to the returned function, so you can spread any sorting function built with `constructSortFn` and override only what differs. + +For example, a version of `alphanumeric` that ignores diacritics, so that "Éric Bernard" sorts next to "Eric Brandon" instead of after "Zak O'Sullivan": + +```tsx +const stripDiacritics = (value: string) => + value.normalize('NFD').replace(/\p{Diacritic}/gu, '') + +const alphanumericIgnoreDiacritics = constructSortFn({ + ...sortFn_alphanumeric, // reuse the comparator + resolveDataValue: (value) => + stripDiacritics(sortFn_alphanumeric.resolveDataValue!(value)), +}) + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns: { alphanumeric: sortFn_alphanumeric, alphanumericIgnoreDiacritics }, +}) +``` + +The same pattern works when defining a new sorting function from scratch: + +```tsx +const byLastName = constructSortFn({ + sort: (dataValueA, dataValueB) => + dataValueA === dataValueB ? 0 : dataValueA > dataValueB ? 1 : -1, + resolveDataValue: (value) => + String(value ?? '') + .split(' ') + .at(-1) ?? '', +}) +``` + +### Customize Sorting + +There are a lot of table and column options that you can use to further customize the sorting UX and behavior. + +#### Disable Sorting + +You can disable sorting for either a specific column or the entire table using the `enableSorting` column option or table option. + +```tsx +const columns = [ + { + header: () => 'ID', + accessorKey: 'id', + enableSorting: false, // disable sorting for this column + }, + { + header: () => 'Name', + accessorKey: 'name', + }, + //... +] +//... +const table = useTable({ + features, + columns, + data, + enableSorting: false, // disable sorting for the entire table +}) +``` + +#### Sorting Direction + +By default, the first sorting direction when cycling through the sorting for a column using the `toggleSorting` APIs is ascending for string columns and descending for number columns. You can change this behavior with the `sortDescFirst` column option or table option. + +```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, + 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. + +If not specified, the default value for `sortUndefined` is `1`, and undefined values will be sorted with lower priority (descending), if ascending, undefined will appear on the end of the list. + +- `'first'` - Undefined values will be pushed to the beginning of the list +- `'last'` - Undefined values will be pushed to the end of the list +- `false` - Undefined values will be 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 (the first direction depends on the column's data type and the `sortDescFirst` option, as discussed [above](#sorting-direction); a string column is shown here): + +`'none' -> 'asc' -> 'desc' -> 'none' -> 'asc' -> 'desc' -> ...` + +If you disable sorting removal, the `'none'` state is skipped after the first sort: + +`'none' -> 'asc' -> 'desc' -> 'asc' -> 'desc' -> ...` + +Once a column is sorted and `enableSortingRemoval` is `false`, toggling the sorting on that column will never remove the sorting. However, if the user sorts by another column and it is not a multi-sort event, then the sorting will be removed from the previous column and just applied to the new column. + +> Set `enableSortingRemoval` to `false` if you want to ensure that at least one column is always sorted. + +```tsx +const table = useTable({ + features, + columns, + data, + enableSortingRemoval: false, // disable the ability to remove sorting on columns (sorting can never return to 'none' once applied) +}) +``` + +#### Multi-Sorting + +Sorting by multiple columns at once is enabled by default if using the `column.getToggleSortingHandler` API. If the user holds the `Shift` key while clicking on a column header, the table will sort by that column in addition to the columns that are already sorted. If you use the `column.toggleSorting` API, you have to manually pass in whether or not to use multi-sorting. (`column.toggleSorting(desc, multi)`). + +##### Disable Multi-Sorting + +You can disable multi-sorting for either a specific column or the entire table using the `enableMultiSort` column option or table option. Disabling multi-sorting for a specific column will replace all existing sorting with the new column's sorting. + +```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, + 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, + 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, + 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, + 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/octane/guide/table-context.md b/docs/framework/octane/guide/table-context.md new file mode 100644 index 0000000000..fea627aa4a --- /dev/null +++ b/docs/framework/octane/guide/table-context.md @@ -0,0 +1,153 @@ +--- +title: Table Context Guide +--- + +When you register reusable components with [`createTableHook`](./composable-tables), those components read the current table, cell, or header from Octane context instead of from props. This guide explains how that context works, how to read it with full type-safety, and how to create an isolated context for advanced cases like nested tables. + +## How The Context Is Provided + +The components returned by `useAppTable` are what put values on context: + +- `` provides the current table instance. +- `` provides the current cell instance. +- `` and `` provide the current header instance. + +Your registered `tableComponents`, `cellComponents`, and `headerComponents` then read those instances with the matching hooks: + +```tsx +function PaginationControls() @{ + const table = useTableContext() // the table provided by + +} + +function TextCell() @{ + const cell = useCellContext() // the cell provided by + {String(cell.getValue())} +} + +function SortIndicator() @{ + const header = useHeaderContext() // the header provided by + {header.column.getIsSorted() ? '🔼' : null} +} +``` + +The instance you read back is the same one those wrappers render with, already extended with the `App*` components and your registered components. For example, inside a table component `table.PaginationControls` and `table.AppCell` are available, and inside a cell component `cell.TextCell` and `cell.FlexRender` are available. + +> [!TIP] +> You often do not need table context to reach the table at all. Every `cell`, `header`, `row`, and `column` instance already carries a reference back to its table: `cell.table`, `header.table`, `row.table`, and `column.table`. So inside a cell or header component you can read the table straight off the instance you already have (for example `cell.table.nextPage()`) instead of calling `useTableContext()`. The instances are linked to each other too (`cell.row`, `cell.column`, `header.column`, `row.getAllCells()`), so a single instance is usually enough to navigate to whatever you need. Reach for `useTableContext()` when a component has no instance to start from, such as a standalone toolbar or pagination control. + +## Import The Hooks From Your createTableHook Result + +> [!IMPORTANT] +> For the best type-safety, import `useTableContext`, `useCellContext`, and `useHeaderContext` from the same module where you called `createTableHook`. Those hooks know your `TFeatures` **and** your registered component maps, so `table.PaginationControls`, `cell.TextCell`, and `header.SortIndicator` are all typed. + +```tsx +// hooks/table.ts +export const { + useAppTable, + createAppColumnHelper, + useTableContext, // <- typed with TFeatures + your tableComponents + useCellContext, // <- typed with TFeatures + your cellComponents + useHeaderContext, // <- typed with TFeatures + your headerComponents +} = createTableHook({ + features, + tableComponents, + cellComponents, + headerComponents, +}) +``` + +## Passing Instances Through Your Own Context + +`createTableHookContexts` is also useful on its own, separately from `createTableHook`, for avoiding prop drilling. The `column`, `row`, `cell`, and `header` instances are **stable references**. You can place any of them on a context and read it deep in a subtree, and because the reference does not change when table state changes, the context value stays the same and consumers will not re-render from state changes. + +Reach for `createTableHookContexts` instead of hand-rolling `createContext`: it hands you a context plus a matching `TFeatures`-typed hook, so you do not have to retype the value or write the `useContext` guard yourself. (See [Scoped Contexts With createTableHookContexts](#scoped-contexts-with-createtablehookcontexts) below for the other use of this util, passing the contexts into `createTableHook`.) + +```tsx +// cell-slot-context.ts +export const { cellContext, useCellContext } = + createTableHookContexts() + +// provide the (stable) cell once, near where you render it +function CellSlot({ cell }: { cell: Cell }) @{ + + + +} + +// read it anywhere below, no prop drilling +function DeeplyNestedCellUI() @{ + const cell = useCellContext() // typed with your TFeatures + {/* ... */} +} +``` + +> [!IMPORTANT] +> That stability is exactly why state-dependent method reads go stale. A memoized component that reads something like `header.column.getIsSorted()`, `cell.row.getIsSelected()`, or `cell.getValue()` off a context-provided instance will not re-render when that state changes on its own. Wrap reactive reads in `Subscribe` (or `useSelector`) so they re-run on the state they depend on. See the [table state guide](./table-state) for `Subscribe` and selector patterns. + +The `table` object is the exception. `useTable` returns a fresh `table` reference on every state change, so that JSX which depends on it re-evaluates. It is therefore **not** a stable context value: providing the table through your own context re-renders consumers whenever the providing component re-renders. If you want a stable handle to pass down, provide `table.store` or a specific atom such as `table.atoms.rowSelection` (these are stable) and read them with `Subscribe` / `useSelector`, or keep the table in a ref. + +## The Default Shared Context + +By default you do not create or wire any context yourself. `createTableHook` connects its `AppTable`/`AppCell`/`AppHeader` providers to a shared, module-scoped context, and the hooks it returns read from that same context. Because the context lives at module scope, its identity is stable across hot module replacement during local development, so a component edit never leaves a provider and a consumer pointing at mismatched contexts. + +Independent tables stay isolated through normal provider scoping: each `` provides its own table instance, and a consumer reads the nearest provider. Rendering two different tables side by side, or in different routes, works without any extra setup. + +## Scoped Contexts With createTableHookContexts + +The shared context only becomes a problem if you **nest** one table's provider inside another's and a consumer in that overlap reads the nearest (inner) provider instead of the one you intended. For that case, create an isolated context with `createTableHookContexts` and pass it into `createTableHook`: + +```tsx +// scoped-table-context.ts (no component imports) +import { createTableHookContexts, tableFeatures } from '@tanstack/octane-table' + +export const features = tableFeatures({ + /* ... */ +}) + +export const { + tableContext, + cellContext, + headerContext, + useTableContext, + useCellContext, + useHeaderContext, +} = createTableHookContexts() +``` + +```tsx +// table.ts +import { createTableHook } from '@tanstack/octane-table' +import { + cellContext, + features, + headerContext, + tableContext, +} from './scoped-table-context' + +export const { useAppTable } = createTableHook({ + features, + tableContext, // <- providers now use these scoped contexts + cellContext, + headerContext, + tableComponents: { + /* ... */ + }, +}) +``` + +Each call to `createTableHookContexts` returns a brand-new set of contexts, so two table setups that use their own scoped contexts can be nested without reading each other's values. + +### Which Hooks To Use With Scoped Contexts + +`createTableHookContexts` also returns `useTableContext`/`useCellContext`/`useHeaderContext`, but they are typed with `TFeatures` only. They do not know your registered component maps, because those are defined later in `createTableHook`. So: + +- Prefer the hooks returned from your `createTableHook` call for the richest types (your `tableComponents`/`cellComponents`/`headerComponents` are typed). +- Use the hooks from `createTableHookContexts` only when you need to read context from a module that cannot import the `createTableHook` result. + +This mirrors TanStack Form's `createFormHookContexts`, where the context hooks are intentionally loose and the strongly typed path lives on the form/field components. + +## When To Use This + +- Do nothing special for the common case. The default shared context handles single tables and multiple independent tables. +- Reach for `createTableHookContexts` only when you nest different table setups and need their contexts isolated, or when you want to read context from a component-free module. diff --git a/docs/framework/octane/guide/table-state.md b/docs/framework/octane/guide/table-state.md new file mode 100644 index 0000000000..436a957cda --- /dev/null +++ b/docs/framework/octane/guide/table-state.md @@ -0,0 +1,430 @@ +--- +title: Table State (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these examples: + +- [Basic useTable](../examples/basic-use-table) +- [Basic External Atoms](../examples/basic-external-atoms) +- [Basic External State](../examples/basic-external-state) +- [Basic Subscribe](../examples/basic-subscribe) + +## Table State (Octane) Guide + +> **If you boil TanStack Table down to one sentence: TanStack Table is a large state-management coordinator for table states.** + +Understanding this guide is fundamental to understanding how TanStack Table works and how to interact with it for the best results. + +### Do you need to Manage External State? + +You usually do NOT need to manage table state yourself. If you pass nothing to `initialState`, `atoms`, `state`, or any of the `on[State]Change` table options, TanStack Table will manage its own state internally. + +There will be situations where you need to customize how you interact with the internal table state, or even hoist it up to your own scopes. TanStack Table lets you read, subscribe to, or own the state slices that matter to your app. This guide explains how table state works in Octane, how to read it, and when to use external atoms or external state. + +### State in v9 + +TanStack Table v9 overhauled state management around TanStack Store. TanStack Store uses the `alien-signals` implementation and supports performant derived state. For TanStack Table, this means the table can derive a full state store from independent state atoms and Octane can subscribe to only the pieces of table state that a component actually needs. + +A table instance has a few state surfaces: + +- `table.baseAtoms` are the internal writable atoms created from the resolved initial state. +- `table.atoms` are readonly derived atoms exposed per registered state slice. +- `table.store` is a readonly flat TanStack Store derived by putting all of the registered `table.atoms` together. +- `table.state` is the Octane adapter's selected state. Its type and value come from the selector passed as the second argument to `useTable`. + +The important change from previous versions is that table state is now atomic. Octane can subscribe to all selected state, a selected subset of state, or a single atom such as `table.atoms.rowSelection`. + +### Feature-based State + +State slices are only created for the features that are registered in `features`. This keeps TanStack Table tree-shakeable and gives TypeScript more accurate state inference. + +For example, if `features` includes `rowPaginationFeature`, TypeScript exposes pagination state APIs and `table.atoms.pagination`. If `features` does not include `rowPaginationFeature`, `pagination` should not be available in `table.atoms`, `table.store.state`, `table.state`, `initialState`, `state`, or `atoms`. + +```tsx +const features = tableFeatures({ + rowPaginationFeature, + rowSortingFeature, + paginatedRowModel: createPaginatedRowModel(), + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const table = useTable({ + features, + columns, + data, +}) + +table.atoms.pagination.get() +table.atoms.sorting.get() + +// table.atoms.rowSelection // TypeScript error unless rowSelectionFeature is registered +``` + +The same feature-based typing applies to built-in features and custom feature-provided state. + +### Accessing Table State + +There are two different questions when reading table state: + +- Do you only need the current value? +- Or should this Octane component re-render when that value changes? + +Use a direct atom or store read for the current value. Use a selector subscription for reactive rendering. + +#### Reading State Without Subscribing + +The simplest and most performant way to read a current state value is to read the matching atom: + +```tsx +const pagination = table.atoms.pagination.get() +const sorting = table.atoms.sorting.get() +``` + +You can also read the current flat store snapshot: + +```tsx +const tableState = table.store.state +const pagination = table.store.state.pagination +``` + +These reads are not Octane subscriptions. Calling `table.atoms.pagination.get()` or `table.store.state.pagination` during render reads the current value, but future changes will not automatically re-render that component unless something else causes a render. If the UI needs to stay reactive to table state changes, use `useTable` state selection, `table.Subscribe`, or even a `useSelector` hook from TanStack Store. + +#### Reading Reactive State with useTable + +The second argument to `useTable` is a TanStack Store selector. By default, the selector effectively selects all registered table state, so `table.state` contains the full state and the component re-renders when any selected state changes. + +You can pass your own selector to make `table.state` contain only the reactive state values that you want to cause re-renders. The Octane adapter compares selected values shallowly. The default selector selects all registered table state. + +```tsx +const table = useTable( + { + features, + columns, + data, + }, + (state) => ({ + pagination: state.pagination, + }), +) + +table.state.pagination +``` + +For large tables, you can also opt the parent table component out of table-state re-renders and subscribe lower in the tree: + +```tsx +const table = useTable( + { + features, + columns, + data, + }, + () => null, +) +``` + +With this pattern, the parent component will not re-render for table state changes. Put reactive reads inside `table.Subscribe` where the UI actually needs them. + +#### Optimizing Re-renders with Selectors and table.Subscribe + +Use `table.Subscribe` when you want table-state re-renders to happen at a specific place in the Octane tree. This is useful for large or expensive tables, but it is usually something to reach for after the default `useTable` selector becomes a visible performance issue. + +Without a `source` prop, `table.Subscribe` subscribes to `table.store` and requires a selector. With a `source` prop, it can subscribe directly to one atom or store, such as `table.atoms.rowSelection`. + +```tsx +const table = useTable( + { + features, + columns, + data, + }, + () => null, +) + + ({ + columnFilters: state.columnFilters, + globalFilter: state.globalFilter, + pagination: state.pagination, + })} +> + {() => } + + +function SubscribedRows({ table }) @{ + + @for (const row of table.getRowModel().rows; key row.id) { + ... + } + +} +``` + +You can also subscribe directly to a single atom and select one value from it: + +```tsx + rowSelection[row.id]} +> + {(isSelected) => ( + + )} + +``` + +Advanced subscription patterns require understanding which table APIs depend on which state slices. For example, a row model may depend on filtering, sorting, and pagination, while one row selection checkbox may only need one row's selection value. Start with the default selector, then optimize with selectors and `table.Subscribe` where render cost matters. + +### Setting Table State + +You should almost never need to set table state directly. TanStack Table features expose dedicated APIs for interacting with their state, and those APIs are the safest way to make changes because they preserve the feature's own rules and related updates. + +For example, use pagination APIs instead of mutating pagination state yourself: + +```tsx +table.nextPage() +table.previousPage() +table.setPageIndex(0) +table.setPageSize(25) +``` + +The same idea applies across features. Use APIs like `table.setSorting(...)`, `table.setColumnFilters(...)`, `column.toggleVisibility()`, or `row.toggleSelected()` instead of manually editing the underlying state object. + +If you only care about setting starting values, use `initialState`. If you want to reset a state slice back to its initial value, use that feature's reset API. + +If you really do need to write a state slice directly, the low-level write surface for internally owned state is the matching base atom: + +```tsx +table.baseAtoms.pagination.set((old) => ({ + ...old, + pageIndex: 0, +})) +``` + +Direct base atom writes should be rare. If a slice is owned by an external atom passed through `atoms`, write to that external atom instead; `table.atoms.pagination` will read from the external atom, not the internal base atom. + +### Custom Initial State + +If you only need to customize the starting value for some table state, use `initialState`. You still do not need to manage that state yourself. + +`initialState` only applies to registered state slices. It is used to create the table's initial state and is also used by reset APIs such as `table.resetSorting()` or `table.resetPagination()`. Changing the `initialState` object later does not reset table state. + +```tsx +const table = useTable({ + features, + columns, + data, + initialState: { + sorting: [ + { + id: 'age', + desc: true, + }, + ], + pagination: { + pageIndex: 0, + pageSize: 25, + }, + }, +}) +``` + +> **Note:** Do not provide the same state slice in multiple ownership places unless you intentionally want one to win. For a slice like `pagination`, prefer exactly one of `initialState.pagination`, `atoms.pagination`, or `state.pagination` as the source of truth. External atoms take precedence over external `state`; external `state` syncs into the table's internal base atom. + +#### Resetting to Initial State + +Feature reset APIs reset to `table.initialState` by default. Many reset APIs also accept `true` to reset to that feature's blank/default state instead: + +```tsx +table.resetSorting() +table.resetPagination() +table.resetPagination(true) +``` + +Slice reset APIs like `resetPagination()` update through that feature's state updater and can update an externally owned atom. The core `table.reset()` API resets the internal base atoms, so do not use it as the primary way to reset state that is owned by external atoms. + +### Controlled State + +If you need easy access to table state in other parts of your application, you can control individual state slices. In v9, external atoms are the recommended way to do this because they preserve the atomic state model and let Octane subscribe to exactly the slices it needs. + +#### External Atoms + +Use external atoms when the app should own one or more table state slices. Create stable writable atoms with `useCreateAtom` from TanStack Store, pass them to the table's `atoms` option, and subscribe to them with `useSelector` anywhere else in your app. + +This is especially useful for server-side data fetching. Pagination, sorting, or filters often belong in a query key, and external atoms let the app and the table share those values without lifting the entire table state through Octane state. + +```tsx +import { useCreateAtom, useSelector } from '@tanstack/octane-store' +import { useEffect, useState } from 'octane' +import { + rowPaginationFeature, + tableFeatures, + useTable, + type PaginationState, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + rowPaginationFeature, + // no paginatedRowModel: manual server-side pagination does not need it +}) + +function App() @{ + const paginationAtom = useCreateAtom({ + pageIndex: 0, + pageSize: 10, + }) + + const pagination = useSelector(paginationAtom) + + const [result, setResult] = useState({ rows: [], rowCount: 0 }) + + useEffect(() => { + let cancelled = false + fetchData(pagination).then((nextResult) => { + if (!cancelled) setResult(nextResult) + }) + return () => { + cancelled = true + } + }, [pagination]) + + const table = useTable({ + features, + columns, + data: result.rows, + rowCount: result.rowCount, + atoms: { + pagination: paginationAtom, + }, + manualPagination: true, + }) + + // table pagination APIs update paginationAtom +} +``` + +When using the `atoms` option for a slice, you do not need to add the matching `on[State]Change` option. For example, if you pass `atoms.pagination`, table pagination APIs update that atom directly. + +#### External State + +The classic `state` plus `on[State]Change` pattern is still supported. This can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. Octane state updates re-render the owner component, and that render cannot be avoided by the `useTable` selector in the same way atom subscriptions can be placed lower in the tree. + +To control a slice with external state, pass both the state value and the matching callback: + +```tsx +const [sorting, setSorting] = useState([]) +const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, +}) + +const table = useTable({ + features, + columns, + data, + state: { + sorting, + pagination, + }, + onSortingChange: setSorting, + onPaginationChange: setPagination, +}) +``` + +The v8-style `onStateChange` option (a single global state callback) is gone in v9. It is not supported by `useTable`. Use per-slice `on[State]Change` callbacks paired with `state.`, or external atoms via the `atoms` option. If you truly need to observe every state change, subscribe to `table.store` directly. + +#### Render and Commit Timing + +During an Octane render, `useTable` stages the newest options immediately, so table reads in that same render see fresh data, columns, callbacks, and controlled state. It does not publish that controlled state to `table.store` while the component is rendering. + +After Octane accepts the render, a layout effect marks its snapshot committed and publishes the captured controlled state. Suspended or abandoned renders therefore cannot leak speculative state. The owner drops the matching post-commit notification because it has already rendered that snapshot, while independently mounted `table.Subscribe` consumers still receive the update before paint. + +This commit behavior applies to controlled `state` options. External atoms remain the direct source of truth and table APIs write to them synchronously. + +##### On State Change Callbacks + +The `on[State]Change` callbacks are useful when you are controlling a matching slice through the `state` option. They work like Octane state setters: an updater can be a raw value or a function that receives the previous value and returns the next value. + +If you provide an `on[State]Change` callback, also provide the corresponding value in `state`. For example, `onSortingChange` should be paired with `state.sorting`. + +```tsx +const [sorting, setSorting] = useState([]) + +const table = useTable({ + features, + columns, + data, + state: { + sorting, + }, + onSortingChange: setSorting, +}) +``` + +If you need to add logic inside a callback, check whether the incoming updater is a function or a value: + +```tsx +const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, +}) + +const table = useTable({ + features, + columns, + data, + state: { + pagination, + }, + onPaginationChange: (updater) => { + setPagination((old) => { + const next = updater instanceof Function ? updater(old) : updater + + // side effects or validation can happen here + + return next + }) + }, +}) +``` + +### State Types + +Most complex states in TanStack Table have their own TypeScript types that you can import and use. This is useful for Octane state, external atoms, and helper functions that work with table state. + +```tsx +import { + useTable, + type PaginationState, + type RowSelectionState, + type SortingState, + type TableState, +} from '@tanstack/octane-table' + +const [sorting, setSorting] = useState([ + { + id: 'age', + desc: true, + }, +]) +``` + +`TableState` is inferred from the features registered on that table: + +```tsx +const features = tableFeatures({ + rowPaginationFeature, + rowSortingFeature, +}) + +type MyTableState = TableState +``` + +Prefer feature-specific state types like `SortingState`, `PaginationState`, or `RowSelectionState` when you are creating local state or external atoms for one slice. diff --git a/docs/framework/octane/quick-start.md b/docs/framework/octane/quick-start.md new file mode 100644 index 0000000000..7b4440b505 --- /dev/null +++ b/docs/framework/octane/quick-start.md @@ -0,0 +1,179 @@ +--- +title: Quick Start +--- + +TanStack Table is a headless table library. It manages your table's state and logic (sorting, filtering, pagination, selection, and more) while you keep 100% control over the markup and styles. This page gets you from install to a rendering Octane table, then shows how to layer on your first feature. + +## Installation + +TanStack Table v9 is currently published under the `beta` tag: + +```bash +npm install @tanstack/octane-table@beta +``` + +## Your First Table + +The component below is complete. Paste it into an Octane `.tsrx` file and you will see a working table. Octane components use a `@{ ... }` component body, and keyed `@for` blocks render dynamic lists. + +```tsx +import { tableFeatures, useTable } from '@tanstack/octane-table' +import type { ColumnDef } from '@tanstack/octane-table' + +// 1. Define the shape of your data +type Person = { + firstName: string + lastName: string + age: number +} + +// 2. Give your data a stable reference (module scope, useState, a query cache, etc.) +const data: Array = [ + { firstName: 'tanner', lastName: 'linsley', age: 24 }, + { firstName: 'tandy', lastName: 'miller', age: 40 }, + { firstName: 'joe', lastName: 'dirte', age: 45 }, +] + +// 3. New in v9: declare which features this table uses (none yet) +const features = tableFeatures({}) + +// 4. Define your columns +const columns: Array> = [ + { + accessorKey: 'firstName', // accessorKey shorthand + header: 'First Name', + cell: (info) => info.getValue(), + }, + { + accessorFn: (row) => row.lastName, // accessorFn alternative with a custom id + id: 'lastName', + header: () => Last Name, + cell: (info) => {info.getValue()}, + }, + { + accessorKey: 'age', + header: () => 'Age', + }, +] + +export function PersonTable() @{ + // 5. Create the table instance + const table = useTable({ + features, + columns, + data, + }) + + // 6. Render markup from the table instance APIs +
+``` + +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 +} +``` + +You rarely need to manage this transient drag state yourself, but if you do, the recommended v9 approach is an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can observe the resize state without re-rendering the component that owns the table. + +```tsx +import { useCreateAtom, useSelector } from '@tanstack/octane-store' +import type { columnResizingState } from '@tanstack/octane-table' + +const columnResizingAtom = useCreateAtom({ + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, +}) + +const columnResizing = useSelector(columnResizingAtom) // subscribe wherever it is needed + +const table = useTable({ + features, + columns, + data, + atoms: { + columnResizing: columnResizingAtom, + }, +}) +``` + +Alternatively, the v8-style `state.columnResizing` plus `onColumnResizingChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```tsx +const [columnResizing, setColumnResizing] = useState({ + columnSizingStart: [], + deltaOffset: null, + deltaPercentage: null, + isResizingColumn: false, + startOffset: null, + startSize: null, +}) + +const table = useTable({ + features, + 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 through `table.setColumnResizing`. + +```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 Octane, an `"onChange"` resize can re-render the whole table on every drag frame, which degrades performance. The [performant column resizing example](../examples/column-resizing-performant) shows how to keep a drag off Octane's render path entirely, so even a table with expensive cells stays smooth. + +The idea is to stop treating column widths as component state during a drag: + +1. **Subscribe the table component to no resize state.** Pass a selector that returns a constant (the example uses `() => ({})`) so state changes never re-render the component that owns the table. +2. **Write column widths as CSS variables imperatively.** In a `useLayoutEffect` (from `octane/hooks`), subscribe to `table.atoms.columnSizing` and set `--header--size` and `--col--size` variables directly on the `` element. Cells reference them with `width: calc(var(--col-firstName-size) * 1px)`, so the browser applies new widths with zero Octane work per frame. +3. **Isolate the few things that must react into `table.Subscribe` islands.** The active resizer's highlight and any live state readout each subscribe to just the slice they need, so a drag re-renders only those small islands, not the table body. + +This replaces the older "memoize the table body while resizing" approach. Because the body no longer subscribes to resize state, it only re-renders when your data changes. diff --git a/docs/framework/octane/guide/column-sizing.md b/docs/framework/octane/guide/column-sizing.md new file mode 100644 index 0000000000..4a5e6ccd52 --- /dev/null +++ b/docs/framework/octane/guide/column-sizing.md @@ -0,0 +1,169 @@ +--- +title: Column Sizing (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Column Sizing](../examples/column-sizing) + +### Column Sizing Setup + +Here's how you set up your table to use column sizing features. Adding the column sizing feature enables the related APIs. + +```tsx +import { + useTable, + tableFeatures, + columnSizingFeature, +} from '@tanstack/octane-table' + +const features = tableFeatures({ columnSizingFeature }) + +const table = useTable({ + features, + columns, + data, +}) +``` + +## Column Sizing (Octane) 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 features = tableFeatures({ columnSizingFeature }) + +const columns = [ + { + accessorKey: 'col1', + size: 270, //set column size for this column + }, + //... +] + +const table = useTable({ + features, + defaultColumn: { + size: 200, // starting column size + minSize: 50, // enforced during column resizing + maxSize: 500, // enforced during column resizing + }, + //... +}) +``` + +The column "sizes" are stored in the table state as numbers, and are usually interpreted as pixel unit values, but you can hook up these column sizing values to your css styles however you see fit. + +As a headless utility, table logic for column sizing is really only a collection of states that you can apply to your own layouts how you see fit (our example above implements 2 styles of this logic). You can apply these width measurements in a variety of ways: + +- semantic `table` elements or any elements being displayed in a table css mode +- `div/span` elements or any elements being displayed in a non-table css mode + - Block level elements with strict widths + - Absolutely positioned elements with strict widths + - Flexbox positioned elements with loose widths + - Grid positioned elements with loose widths +- Really any layout mechanism that can interpolate cell widths into a table structure. + +Each of these approaches has its own tradeoffs and limitations which are usually opinions held by a UI/component library or design system, luckily not you 😉. + +### Column Sizing APIs + +Use the column and header APIs to read the calculated size and offsets for rendering. These values come from the `columnSizing` state and the column definition defaults. + +```tsx +column.getSize() +header.getSize() + +column.getStart() // start offset in the current column flow +column.getStart('start') +column.getStart('center') +column.getStart('end') + +column.getAfter() // end offset in the current column flow +column.getAfter('start') +column.getAfter('center') +column.getAfter('end') + +column.resetSize() +``` + +The table instance also exposes total size helpers. These are useful when building scroll containers, split pinned-column tables, or CSS variables for column widths. + +```tsx +table.getTotalSize() +table.getStartTotalSize() +table.getCenterTotalSize() +table.getEndTotalSize() +``` + +If you need to update sizing state directly, use `table.setColumnSizing`. Use `table.resetColumnSizing` to reset to `initialState.columnSizing`, or pass `true` to reset to the feature default. + +```tsx +table.setColumnSizing({ + firstName: 180, + age: 80, +}) + +table.resetColumnSizing() +table.resetColumnSizing(true) +``` + +### Managing Column Sizing State + +If you need to own the `columnSizing` state yourself (for example, to persist user-set column widths), the recommended v9 approach is an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the sizing state without re-rendering the component that owns the table. + +```tsx +import { useCreateAtom, useSelector } from '@tanstack/octane-store' +import type { ColumnSizingState } from '@tanstack/octane-table' + +const features = tableFeatures({ columnSizingFeature }) + +const columnSizingAtom = useCreateAtom({}) + +const columnSizing = useSelector(columnSizingAtom) // subscribe wherever it is needed + +const table = useTable({ + features, + columns, + data, + atoms: { + columnSizing: columnSizingAtom, + }, +}) +``` + +Alternatively, the v8-style `state.columnSizing` plus `onColumnSizingChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```tsx +const features = tableFeatures({ columnSizingFeature }) + +const [columnSizing, setColumnSizing] = useState({}) + +const table = useTable({ + features, + columns, + data, + state: { + columnSizing, + }, + onColumnSizingChange: setColumnSizing, +}) +``` diff --git a/docs/framework/octane/guide/column-visibility.md b/docs/framework/octane/guide/column-visibility.md new file mode 100644 index 0000000000..9c0f95eb41 --- /dev/null +++ b/docs/framework/octane/guide/column-visibility.md @@ -0,0 +1,182 @@ +--- +title: Column Visibility (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Column Visibility](../examples/column-visibility) + +### Column Visibility Setup + +Here's how you set up your table to use column visibility features. Adding the column visibility feature enables the related APIs. + +```tsx +import { + useTable, + tableFeatures, + columnVisibilityFeature, +} from '@tanstack/octane-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +const table = useTable({ + features, + columns, + data, +}) +``` + +## Column Visibility (Octane) Guide + +The column visibility feature allows table columns to be hidden or shown dynamically. In v9, add `columnVisibilityFeature` to your `features` to enable this. There is a dedicated `columnVisibility` state and APIs for managing column visibility dynamically. + +### Column Visibility State + +The `columnVisibility` state is a map of column IDs to boolean values. A column will be hidden if its ID is present in the map and the value is `false`. If the column ID is not present in the map, or the value is `true`, the column will be shown. + +If you need to own the `columnVisibility` state yourself (for example, to persist user preferences), the recommended v9 approach is an external atom passed to the table's `atoms` option. External atoms give you fine-grained subscriptions anywhere in your app, and other code can read or write the visibility state without re-rendering the component that owns the table. + +```tsx +import { useCreateAtom, useSelector } from '@tanstack/octane-store' +import { + useTable, + tableFeatures, + columnVisibilityFeature, +} from '@tanstack/octane-table' +import type { ColumnVisibilityState } from '@tanstack/octane-table' + +const features = tableFeatures({ columnVisibilityFeature }) + +const columnVisibilityAtom = useCreateAtom({ + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, +}) + +const columnVisibility = useSelector(columnVisibilityAtom) // subscribe wherever it is needed + +const table = useTable({ + features, + //... + atoms: { + columnVisibility: columnVisibilityAtom, + }, +}) +``` + +Alternatively, the v8-style `state.columnVisibility` plus `onColumnVisibilityChange` pattern is still supported. It can be convenient for simple integrations or when migrating v8 code, but it is less fine-grained than external atoms. See the [Table State Guide](./table-state) for a deeper comparison. + +```tsx +const features = tableFeatures({ columnVisibilityFeature }) + +const [columnVisibility, setColumnVisibility] = useState( + { + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, + }, +) + +const table = useTable({ + features, + //... + 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 features = tableFeatures({ columnVisibilityFeature }) + +const table = useTable({ + features, + //... + initialState: { + columnVisibility: { + columnId1: true, + columnId2: false, // hide this column by default + columnId3: true, + }, + //... + }, +}) +``` + +### Disable Hiding Columns + +By default, all columns can be hidden or shown. If you want to prevent certain columns from being hidden, you set the `enableHiding` column option to `false` for those columns. + +```tsx +const columns = [ + { + header: 'ID', + accessorKey: 'id', + enableHiding: false, // disable hiding for this column + }, + { + header: 'Name', + accessorKey: 'name', // can be hidden + }, +] +``` + +### Column Visibility Toggle APIs + +There are several column API methods that are useful for rendering column visibility toggles in the UI. + +- `column.getCanHide` - Useful for disabling the visibility toggle for a column that has `enableHiding` set to `false`. +- `column.getIsVisible` - Useful for setting the initial state of the visibility toggle. +- `column.toggleVisibility` - Useful for toggling the visibility of a column. +- `column.getToggleVisibilityHandler` - Shortcut for hooking up the `column.toggleVisibility` method to a UI event handler. + +```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/octane/guide/composable-tables.md b/docs/framework/octane/guide/composable-tables.md new file mode 100644 index 0000000000..ec6cc1f2e4 --- /dev/null +++ b/docs/framework/octane/guide/composable-tables.md @@ -0,0 +1,354 @@ +--- +title: Composable Tables (createTableHook) Guide +--- + +`createTableHook` creates an app-specific table factory. Use it to define shared features, row models, and default table options once, then create each Octane table with the columns and data that are unique to that table. + +The same API can also register reusable table, cell, and header components, but component registration is optional. Start with shared options and features first; add reusable components only when your app needs standardized table UI pieces. + +## Examples + +- [Basic useAppTable](../examples/basic-use-app-table) - Minimal `createTableHook` setup. +- [Composable Tables](../examples/composable-tables) - Richer Users and Products tables sharing `src/hooks/table.ts` and reusable components. + +## Start With Shared Features and Options + +Create one app table hook and put the feature set, row models, and shared defaults there. This example makes sorting available to every table created by `useAppTable`. + +```tsx +import { + createSortedRowModel, + createTableHook, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const { useAppTable, createAppColumnHelper } = createTableHook({ + features, + debugTable: true, + enableSortingRemoval: false, +}) +``` + +Options passed to `createTableHook` become defaults for every table created by `useAppTable`. The `features` option is also bound to the returned column helper, so column definitions know that sorting APIs are available. + +## Create App Columns + +Create one column helper per row type. The helper is already bound to your app's feature set, so each table does not need to thread `typeof features` through its column definitions. + +```tsx +type Person = { + firstName: string + lastName: string + age: number + visits: number +} + +const columnHelper = createAppColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + header: () => Last Name, + cell: (info) => {info.getValue()}, + }), + columnHelper.accessor('age', { + header: 'Age', + }), + columnHelper.accessor('visits', { + header: 'Visits', + }), +]) +``` + +## Create A Table + +Create each table with `useAppTable`. The call site provides table-specific inputs such as `columns` and `data`; shared features and defaults come from the hook. + +```tsx +function UsersTable({ data }: { data: Person[] }) @{ + const table = useAppTable( + { + columns, + data, + }, + (state) => ({ sorting: state.sorting }), + ) + + // render with the table instance +} +``` + +## Render With The Normal Table APIs + +You can render the table with the same table instance APIs used by a standalone `useTable` table. This simple path does not require `AppTable`, `AppCell`, `AppHeader`, or registered components. + +```tsx + + + @for (const headerGroup of table.getHeaderGroups(); key headerGroup.id){' '} + { + + @for (const header of headerGroup.headers; key header.id){' '} + { + + } + + } + + + @for (const row of table.getRowModel().rows; key row.id){' '} + { + + @for (const cell of row.getAllCells(); key cell.id){' '} + { + + } + + } + +
+ {header.isPlaceholder ? null : } +
+ +
+``` + +## Override Shared Defaults Per Table + +Options passed to `useAppTable` override defaults from `createTableHook`. Use this for the few tables that need different behavior without creating a separate app hook. + +```tsx +const table = useAppTable( + { + columns, + data, + enableSortingRemoval: true, + }, + (state) => ({ sorting: state.sorting }), +) +``` + +## Optional: Reusable Components + +The richer composable-tables example also uses `createTableHook` as a component registry. Use this when several tables should share the same toolbar controls, cell renderers, header renderers, or footer renderers. + +### Component Registry Setup + +The composable tables example keeps the shared configuration in `src/hooks/table.ts`. + +```tsx +import { + columnFilteringFeature, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + createTableHook, + filterFns, + rowPaginationFeature, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/octane-table' + +import { + PaginationControls, + RowCount, + TableToolbar, +} from '../components/table-components' +import { + CategoryCell, + NumberCell, + PriceCell, + ProgressCell, + RowActionsCell, + StatusCell, + TextCell, +} from '../components/cell-components' +import { + ColumnFilter, + FooterColumnId, + FooterSum, + SortIndicator, +} from '../components/header-components' + +const features = tableFeatures({ + columnFilteringFeature, + rowPaginationFeature, + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortFns, + filterFns, +}) + +export const { + createAppColumnHelper, + useAppTable, + useTableContext, + useCellContext, + useHeaderContext, +} = createTableHook({ + features, + getRowId: (row) => row.id, + tableComponents: { + PaginationControls, + RowCount, + TableToolbar, + }, + cellComponents: { + TextCell, + NumberCell, + StatusCell, + ProgressCell, + RowActionsCell, + PriceCell, + CategoryCell, + }, + headerComponents: { + SortIndicator, + ColumnFilter, + FooterColumnId, + FooterSum, + }, +}) +``` + +### Returned Helpers + +| Helper | Purpose | +| ----------------------- | -------------------------------------------------------------------------------------- | +| `useAppTable` | Creates a table with shared features, row models, defaults, and registered components. | +| `createAppColumnHelper` | Creates column helpers with `TFeatures` and registered component types already bound. | +| `useTableContext` | Reads the current table inside registered table components. | +| `useCellContext` | Reads the current cell inside registered cell components. | +| `useHeaderContext` | Reads the current header/footer inside registered header components. | + +> [!IMPORTANT] +> Import `useTableContext`, `useCellContext`, and `useHeaderContext` from the same module where you called `createTableHook` (as shown above). Those hooks are typed with your `TFeatures` and your registered component maps, so values like `table.PaginationControls`, `cell.TextCell`, and `header.SortIndicator` are typed. See the [Table Context guide](./table-context) for details and for the scoped-context escape hatch. + +### Component Columns + +Create one column helper per row type. The helper is bound to the app table setup, so column definitions can reference registered components directly. + +```tsx +const personColumnHelper = createAppColumnHelper() + +const columns = useMemo( + () => + personColumnHelper.columns([ + personColumnHelper.accessor('firstName', { + header: 'First Name', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + personColumnHelper.accessor('age', { + header: 'Age', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + personColumnHelper.display({ + id: 'actions', + header: 'Actions', + cell: ({ cell }) => , + }), + ]), + [], +) +``` + +Registered cell components use `useCellContext()` internally, and registered header/footer components use `useHeaderContext()`. + +### Component Table Rendering + +Create each table with `useAppTable`. You pass table-specific options like `columns` and `data`; the shared `features` (which includes row model factories), `getRowId`, and component registries come from the hook. + +```tsx +const table = useAppTable( + { + columns, + data, + debugTable: true, + }, + (state) => state, +) +``` + +The returned table includes `AppTable`, `AppHeader`, `AppCell`, and `AppFooter` wrappers. The example uses `AppTable` with a selector so rendering can subscribe to the state slices used by that table. + +```tsx + ({ + pagination: state.pagination, + sorting: state.sorting, + columnFilters: state.columnFilters, + })} +> + {({ sorting, columnFilters }) => ( +
+ + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((h) => ( + + {(header) => ( + + )} + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getAllCells().map((c) => ( + + {(cell) => ( + + )} + + ))} + + ))} + +
+ + + +
+ +
+ + + +
+ )} +
+``` + +### Reusing The Component Registry + +The example creates both `personColumnHelper` and `productColumnHelper` from the same `createAppColumnHelper`, then renders Users and Products tables with the same `useAppTable` factory. Each table owns its data and columns, while the app hook owns table infrastructure and component conventions. + +### Scoped Contexts + +Registered components read the current table, cell, or header from Octane context. By default `createTableHook` wires this up for you with a shared context, and you do not need to think about it. If you nest different table setups and need their contexts isolated, create a scoped context with `createTableHookContexts` and pass it into `createTableHook`. See the [Table Context guide](./table-context) for the full explanation and examples. + +## When To Use This Pattern + +Use `createTableHook` when multiple tables should share features, row models, default options, or conventions. Use the standalone `useTable` API for a one-off table. Add the component registry only when the app wants standardized reusable table UI pieces. diff --git a/docs/framework/octane/guide/custom-features.md b/docs/framework/octane/guide/custom-features.md new file mode 100644 index 0000000000..7eb4ae694f --- /dev/null +++ b/docs/framework/octane/guide/custom-features.md @@ -0,0 +1,373 @@ +--- +title: Custom Features (Octane) Guide +--- + +## Examples + +Want to skip to the implementation? Check out these Octane examples: + +- [Custom Plugin](../examples/custom-plugin) + +## Custom Features (Octane) 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 + initTableInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => void + getDefaultColumnDef?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >() => ColumnDefBase_All + getDefaultTableOptions?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => Partial> + getInitialState?: (initialState: Partial) => TableState_All + initCellInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >( + cell: Cell, + ) => void + initColumnInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >( + column: Column, + ) => void + initHeaderGroupInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + headerGroup: HeaderGroup, + ) => void + initHeaderInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >( + header: Header, + ) => void + initRowInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + row: Row, + ) => void + resetTableInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + >( + table: Table_Internal, + ) => void +} +``` + +This might be a bit confusing, so let's break down what each of these methods 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 Resizing](https://github.com/TanStack/table/blob/beta/packages/table-core/src/features/column-resizing/columnResizingFeature.ts) feature, the `getDefaultTableOptions` method sets the default `columnResizeMode` option with a default value of `"onEnd"`. + +
+ +##### getDefaultColumnDef + +The `getDefaultColumnDef` method in a table feature is responsible for setting the default column options for that feature. For example, in the [Sorting](https://github.com/TanStack/table/blob/beta/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/beta/packages/table-core/src/features/row-pagination/rowPaginationFeature.ts) feature, the `getInitialState` method sets the default `pageSize` state with a value of `10` and the default `pageIndex` state with a value of `0`. + +#### API Creators + +
+ +##### initTableInstanceData and resetTableInstanceData + +Use `initTableInstanceData` for mutable, non-reactive data that belongs to one table instance, such as an interaction anchor or an imperative cache. It runs once after table options, state atoms, and the store have been created. Features are processed in a single pass in registration order; each feature's initialization hook runs just before that feature's `constructTableAPIs` hook, so hooks may rely on data and APIs from features registered earlier. + +Use `resetTableInstanceData` to clear that transient data when `table.reset()` runs. Reset hooks run after internally owned table state atoms have been restored to `table.initialState`. They do not reset table state slices or externally controlled state, and `table.reset()` does not rerun `initTableInstanceData`. + +Keep API assignment in `constructTableAPIs`; initialization and reset hooks are for data owned by the feature. + +
+ +##### constructTableAPIs + +The `constructTableAPIs` method in a table feature is exclusively responsible for adding methods to the `table` instance. It runs after all feature-owned table instance data has been initialized. For example, in the [Row Selection](https://github.com/TanStack/table/blob/beta/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature, the `constructTableAPIs` method adds many table instance API methods such as `toggleAllRowsSelected`, `getIsAllRowsSelected`, `getIsSomeRowsSelected`, etc. So then, when you call `table.toggleAllRowsSelected()`, you are calling a method that was added to the table instance by the `rowSelectionFeature` feature. + +
+ +##### assignHeaderPrototype and initHeaderInstanceData + +The `assignHeaderPrototype` method in a table feature is responsible for adding methods to the shared `header` prototype. For example, the [Column Sizing](https://github.com/TanStack/table/blob/beta/packages/table-core/src/features/column-sizing/columnSizingFeature.ts) feature adds header instance API methods such as `getStart`. So then, when you call `header.getStart()`, you are calling a method that was added by the column sizing feature. The `initHeaderInstanceData` method is available for per-header instance data or caches that cannot live on the shared prototype. It runs during header construction, before sub-headers are populated and before the header is linked to its header group. Headers are reconstructed whenever header groups recompute, so it reruns on every rebuild. + +
+ +##### initHeaderGroupInstanceData + +The `initHeaderGroupInstanceData` method is available for per-header-group instance data. Header groups have no shared prototype, so this is their only per-instance extension point. It runs after a header group's `depth`, `id`, and fully populated `headers` array have been assigned, and reruns whenever header groups are rebuilt. + +
+ +##### assignColumnPrototype and initColumnInstanceData + +The `assignColumnPrototype` method in a table feature is responsible for adding methods to the shared `column` prototype. For example, the [Sorting](https://github.com/TanStack/table/blob/beta/packages/table-core/src/features/row-sorting/rowSortingFeature.ts) feature adds column instance API methods such as `getNextSortingOrder`, `toggleSorting`, etc. So then, when you call `column.toggleSorting()`, you are calling a method that was added by the row sorting feature. The `initColumnInstanceData` method is available for per-column instance data or caches that cannot live on the shared prototype. For example, the [Aggregation](https://github.com/TanStack/table/blob/beta/packages/table-core/src/features/row-aggregation/rowAggregationFeature.ts) feature uses it to set up a per-column aggregation cache. + +
+ +##### assignRowPrototype and initRowInstanceData + +The `assignRowPrototype` method in a table feature is responsible for adding methods to the shared `row` prototype. The `initRowInstanceData` method is available for per-row instance data or caches that cannot live on the shared prototype. For example, the [Row Selection](https://github.com/TanStack/table/blob/beta/packages/table-core/src/features/row-selection/rowSelectionFeature.ts) feature adds row instance API methods such as `toggleSelected` and `getIsSelected`. + +
+ +##### assignCellPrototype and initCellInstanceData + +The `assignCellPrototype` method in a table feature is responsible for adding methods to the shared `cell` prototype. For example, Column Grouping adds `getIsGrouped` and `getIsPlaceholder`, while Aggregation adds `getIsAggregated`. The `initCellInstanceData` method is available for per-cell instance data or caches that cannot live on the shared prototype. Cells are constructed lazily on first access per row/column pair and cached, so it runs once per cell instance. + +### Adding a Custom Feature + +Let's walk through making a custom table feature for a hypothetical use case. Let's say we want to add a feature to the table instance that allows the user to change the "density" (padding of cells) of the table. + +Check out the full [custom-plugin](../examples/custom-plugin) example to see the full implementation, but here's an in-depth look at the steps to create a custom feature. + +#### Step 1: Set up TypeScript Types + +Assuming you want the same full type-safety that the built-in features in TanStack Table have, let's set up all of the TypeScript types for our new feature. We'll create types for new table options, state, and table instance API methods. + +These types are following the naming convention used internally within TanStack Table, but you can name them whatever you want. We are not adding these types to TanStack Table yet, but we'll do that in the next step. + +```ts +// define types for our new feature's custom state +export type DensityState = 'sm' | 'md' | 'lg' +export interface TableState_Density { + density: DensityState +} + +// define types for our new feature's table options +export interface TableOptions_Density { + enableDensity?: boolean + onDensityChange?: OnChangeFn +} + +// Define types for our new feature's table APIs +export interface Table_Density { + setDensity: (updater: Updater) => void + toggleDensity: (value?: DensityState) => void +} +``` + +#### Step 2: Add the Feature to TanStack Table's Feature Maps + +TanStack Table uses the keys passed to `tableFeatures({ ... })` to infer which feature state, options, and APIs exist on a table. To make a custom feature key type-safe, add it to the exported `Plugins`, `TableState_FeatureMap`, `TableOptions_FeatureMap`, and `Table_FeatureMap` interfaces with declaration merging. + +```ts +declare module '@tanstack/octane-table' { + interface Plugins { + densityPlugin: TableFeature + } + + interface TableState_FeatureMap { + densityPlugin: TableState_Density + } + + interface TableOptions_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: TableOptions_Density + } + + interface Table_FeatureMap< + TFeatures extends TableFeatures, + TData extends RowData, + > { + densityPlugin: Table_Density + } +} +``` + +Once the feature is registered this way, TypeScript can infer the feature's state, options, and APIs only on tables whose `features` include `densityPlugin`. + +#### Step 3: Create the Feature Object + +With all of that TypeScript setup out of the way, we can now create the feature object for our new feature. This is where we define all of the methods that will be added to the table instance. + +Use the `TableFeature` type to ensure that you are creating the feature object correctly. If the TypeScript types are set up correctly, you should have no TypeScript errors when you create the feature object with the new state, options, and instance APIs. + +```ts +export const densityPlugin: TableFeature = { + // define the new feature's initial state + getInitialState: (initialState) => { + return { + density: 'md', + ...initialState, // must come last + } + }, + + // define the new feature's default options + getDefaultTableOptions: (table) => { + return { + enableDensity: true, + onDensityChange: makeStateUpdater('density', table), + } + }, + // if you need to add a default column definition... + // getDefaultColumnDef: () => {}, + + // define the new feature's table instance methods + constructTableAPIs: (table) => { + assignTableAPIs('densityPlugin', table, { + table_setDensity: { + fn: (updater: Updater) => { + const safeUpdater: Updater = (old) => { + const newState = functionalUpdate(updater, old) + return newState + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + table_toggleDensity: { + fn: (value?: DensityState) => { + const safeUpdater: Updater = (old) => { + if (value) return value + return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' + } + return table.options.onDensityChange?.(safeUpdater) + }, + }, + }) + }, + + // if you need to add row instance APIs... + // assignRowPrototype: (prototype, table) => {}, + // initRowInstanceData: (row) => {}, + // if you need to add cell instance APIs... + // assignCellPrototype: (prototype, table) => {}, + // initCellInstanceData: (cell) => {}, + // if you need to add column instance APIs... + // assignColumnPrototype: (prototype, table) => {}, + // initColumnInstanceData: (column) => {}, + // if you need to add header instance APIs... + // assignHeaderPrototype: (prototype, table) => {}, + // initHeaderInstanceData: (header) => {}, + // if you need to add header group instance data... + // initHeaderGroupInstanceData: (headerGroup) => {}, +} +``` + +#### Step 4: Add the Feature to the Table + +Now that we have our feature object, we can add it to the table instance by including it in the `tableFeatures()` call and passing the result to the `features` option when we create the table instance. + +```ts +const features = tableFeatures({ densityPlugin }) + +const table = useTable({ + features, + columns, + data, + //.. +}) +``` + +#### Step 5: Use the Feature in Your Application + +Now that the feature is added to the table instance, you can use the new instance APIs, options, and state in your application. The [custom-plugin example](../examples/custom-plugin) controls the `density` state with `useState` and the new `onDensityChange` option: + +```tsx +const features = tableFeatures({ densityPlugin }) + +const [density, setDensity] = useState('md') + +const table = useTable({ + features, + columns, + data, + //... + state: { + density, // passing the density state to the table, TS is still happy :) + }, + onDensityChange: setDensity, +}) +//... +return ( +
+ +
+ +
+ {/* Your custom UI goes here */} +
{/* */}
+ + @for (const headerGroup of table.getHeaderGroups(); key headerGroup.id) { + + @for (const header of headerGroup.headers; key header.id) { + + } + + } + + + @for (const row of table.getRowModel().rows; key row.id) { + + @for (const cell of row.getAllCells(); key cell.id) { + + } + + } + +
+ {header.isPlaceholder ? null : } +
+} +``` + +A few things to note: + +- `tableFeatures({})` declares which optional features the table uses. Registering only what you need keeps bundles small and gives TypeScript accurate types for the table instance. +- The core row model is always included automatically. Feature row models (sorting, filtering, pagination) are registered as slots directly on the `tableFeatures({...})` call when you need them. +- `table.FlexRender` renders the `header`, `cell`, and `footer` definitions from your columns, whether they are plain values or Octane components. +- Octane compiles authored TSRX for the current client or server target. Keep `.tsrx` source in your application compilation path. + +See the full [Basic useTable example](./examples/basic-use-table) for a runnable version with more columns and a footer. + +## Add a Feature: Sorting + +Features are opt-in in v9. To make columns sortable, register `rowSortingFeature` and the sorted row model in `tableFeatures`, then wire the header click handler. + +```tsx +import { + createSortedRowModel, + rowSortingFeature, + sortFns, + tableFeatures, + useTable, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + rowSortingFeature, // enables sorting APIs and state + sortedRowModel: createSortedRowModel(), // client-side sorting + sortFns, // built-in sort functions +}) + +export function PersonTable() @{ + const table = useTable({ + features, + columns, + data, + }) + + + + @for (const headerGroup of table.getHeaderGroups(); key headerGroup.id) { + + @for (const header of headerGroup.headers; key header.id) { + + } + + } + + {/* tbody unchanged from above */} +
+ {header.isPlaceholder ? null : ( +
+ + {header.column.getIsSorted() === 'asc' + ? ' 🔼' + : header.column.getIsSorted() === 'desc' + ? ' 🔽' + : null} +
+ )} +
+} +``` + +Clicking a header now toggles between ascending, descending, and unsorted. Every other feature follows this same pattern: register the feature and its row model factory in `tableFeatures`, then use the APIs those features add to the table, columns, and rows. See the [Sorting Guide](./guide/sorting.md) and the [Sorting example](./examples/sorting) for custom sort functions, multi-sorting, and per-column options. + +## Where to Go Next + +**Table state.** In v9, table state is backed by TanStack Store atoms. You usually do not need to manage it yourself: set `initialState` for starting values and call feature APIs like `table.setSorting(...)` or `table.nextPage()`. When your app should own a state slice, or you want fine-grained subscriptions, read the [Table State Guide](./guide/table-state.md). It is the foundational guide for everything else. + +**Feature guides.** Each feature has its own guide, such as [Column Filtering](./guide/column-filtering.md), [Pagination](./guide/pagination.md), [Row Selection](./guide/row-selection.md), and [Column Visibility](./guide/column-visibility.md). + +**Composable tables.** When multiple tables in your app share features, row models, and component conventions, define them once with `createTableHook`: + +```tsx +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}) + +const { useAppTable, createAppColumnHelper } = createTableHook({ features }) +``` + +See the [Composable Tables Guide](./guide/composable-tables.md) for the full pattern, including pre-bound cell and header components. + +**Examples.** Browse the runnable [Octane examples](./examples/basic-use-table), from basic tables to feature demos, to see intended usage end to end. diff --git a/docs/framework/octane/reference/functions/FlexRender-1.md b/docs/framework/octane/reference/functions/FlexRender-1.md new file mode 100644 index 0000000000..e764abc47d --- /dev/null +++ b/docs/framework/octane/reference/functions/FlexRender-1.md @@ -0,0 +1,55 @@ +--- +id: FlexRender +title: FlexRender +--- + +# Function: FlexRender() + +```ts +function FlexRender(props): unknown; +``` + +Defined in: [FlexRender.ts:54](https://github.com/TanStack/table/blob/main/packages/octane-table/src/FlexRender.ts#L54) + +Simplified component wrapper of [flexRender](flexRender.md). Use this utility component +to render headers, cells, or footers with custom markup. Only one prop +(`cell`, `header`, or `footer`) may be passed. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `unknown` = `unknown` + +## Parameters + +### props + +[`FlexRenderProps`](../type-aliases/FlexRenderProps.md)\<`TFeatures`, `TData`, `TValue`\> + +## Returns + +`unknown` + +## Example + +```tsx + + + +``` + +This replaces calling `flexRender` directly like this: +```tsx +flexRender(cell.column.columnDef.cell, cell.getContext()) +flexRender(header.column.columnDef.header, header.getContext()) +flexRender(footer.column.columnDef.footer, footer.getContext()) +``` diff --git a/docs/framework/octane/reference/functions/createTableHook.md b/docs/framework/octane/reference/functions/createTableHook.md new file mode 100644 index 0000000000..93494318f7 --- /dev/null +++ b/docs/framework/octane/reference/functions/createTableHook.md @@ -0,0 +1,40 @@ +--- +id: createTableHook +title: createTableHook +--- + +# Function: createTableHook() + +```ts +function createTableHook(options): CreateTableHookResult; +``` + +Defined in: [createTableHook.tsrx.d.ts:13](https://github.com/TanStack/table/blob/main/packages/octane-table/src/createTableHook.tsrx.d.ts#L13) + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TTableComponents + +`TTableComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +## Parameters + +### options + +[`CreateTableHookOptions`](../type-aliases/CreateTableHookOptions.md)\<`TFeatures`, `TTableComponents`, `TCellComponents`, `THeaderComponents`\> + +## Returns + +[`CreateTableHookResult`](../interfaces/CreateTableHookResult.md)\<`TFeatures`, `TTableComponents`, `TCellComponents`, `THeaderComponents`\> diff --git a/docs/framework/octane/reference/functions/createTableHookContexts.md b/docs/framework/octane/reference/functions/createTableHookContexts.md new file mode 100644 index 0000000000..3fd15cfad8 --- /dev/null +++ b/docs/framework/octane/reference/functions/createTableHookContexts.md @@ -0,0 +1,69 @@ +--- +id: createTableHookContexts +title: createTableHookContexts +--- + +# Function: createTableHookContexts() + +```ts +function createTableHookContexts(): TableHookContexts; +``` + +Defined in: [createTableHookContexts.ts:60](https://github.com/TanStack/table/blob/main/packages/octane-table/src/createTableHookContexts.ts#L60) + +Creates a fresh, scoped set of table/cell/header contexts (plus matching +context hooks) that you can pass into `createTableHook`. This mirrors +TanStack Form's `createFormHookContexts`. + +You usually do NOT need this: by default `createTableHook` wires its +`AppTable`/`AppCell`/`AppHeader` providers to a shared module-scoped context, +and you read it with the `useTableContext`/`useCellContext`/`useHeaderContext` +hooks returned from `createTableHook`. Reach for `createTableHookContexts` +when you need an *isolated* context, e.g. when nesting one table inside +another and a consumer would otherwise read the wrong (nearest) provider. + +Type-safety note: the hooks returned here are typed with `TFeatures` only. +They do NOT know the component maps you register in `createTableHook` +(`tableComponents`/`cellComponents`/`headerComponents`), because those are +defined later. For the richest types (the `App*` components and your +registered components attached), prefer the `use*Context` hooks returned from +your `createTableHook` call. The hooks here are the escape hatch for reading +context from a module that does not / cannot import the `createTableHook` +result. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` = `RowData` + +## Returns + +[`TableHookContexts`](../interfaces/TableHookContexts.md)\<`TFeatures`, `TData`\> + +## Example + +```ts +// scoped-table-context.ts +export const { + tableContext, + cellContext, + headerContext, + useTableContext, + useCellContext, + useHeaderContext, +} = createTableHookContexts() + +// table.ts +export const { useAppTable } = createTableHook({ + features, + tableContext, // <- pass the scoped contexts so the providers use them + cellContext, + headerContext, + tableComponents: { PaginationControls }, +}) +``` diff --git a/docs/framework/octane/reference/functions/flexRender.md b/docs/framework/octane/reference/functions/flexRender.md new file mode 100644 index 0000000000..2226e339d6 --- /dev/null +++ b/docs/framework/octane/reference/functions/flexRender.md @@ -0,0 +1,48 @@ +--- +id: flexRender +title: flexRender +--- + +# Function: flexRender() + +```ts +function flexRender(Comp, props): unknown; +``` + +Defined in: [FlexRender.ts:24](https://github.com/TanStack/table/blob/main/packages/octane-table/src/FlexRender.ts#L24) + +If rendering headers, cells, or footers with custom markup, use `flexRender` +instead of `cell.getValue()` or `cell.renderValue()`. + +## Type Parameters + +### TProps + +`TProps` *extends* `object` + +## Parameters + +### Comp + +[`Renderable`](../type-aliases/Renderable.md)\<`TProps`\> + +### props + +`TProps` + +## Returns + +`unknown` + +## Example + +```ts +flexRender(cell.column.columnDef.cell, cell.getContext()) + +Port note: upstream additionally detects class components and +`react.memo`/`react.forward_ref` exotic objects. Both branches are dead in +octane — there are no class components or `forwardRef`, and octane's `memo()` +returns a plain function — so a component is exactly `typeof === 'function'`. +The descriptor `createElement` returns renders at value position; non-component +values (strings, numbers, pre-created descriptors) pass through as-is. +``` diff --git a/docs/framework/octane/reference/functions/useTable.md b/docs/framework/octane/reference/functions/useTable.md new file mode 100644 index 0000000000..6e78b68355 --- /dev/null +++ b/docs/framework/octane/reference/functions/useTable.md @@ -0,0 +1,40 @@ +--- +id: useTable +title: useTable +--- + +# Function: useTable() + +```ts +function useTable(tableOptions, selector?): OctaneTable; +``` + +Defined in: [useTable.tsrx.d.ts:14](https://github.com/TanStack/table/blob/main/packages/octane-table/src/useTable.tsrx.d.ts#L14) + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TSelected + +`TSelected` = `TableState`\<`TFeatures`\> + +## Parameters + +### tableOptions + +`TableOptions`\<`TFeatures`, `TData`\> + +### selector? + +(`state`) => `TSelected` + +## Returns + +[`OctaneTable`](../type-aliases/OctaneTable.md)\<`TFeatures`, `TData`, `TSelected`\> diff --git a/docs/framework/octane/reference/index.md b/docs/framework/octane/reference/index.md new file mode 100644 index 0000000000..fa5fc5cfd1 --- /dev/null +++ b/docs/framework/octane/reference/index.md @@ -0,0 +1,56 @@ +--- +id: "@tanstack/octane-table" +title: "@tanstack/octane-table" +--- + +# @tanstack/octane-table + +## Interfaces + +- [AppCellComponent](interfaces/AppCellComponent.md) +- [AppCellContext](interfaces/AppCellContext.md) +- [AppCellPropsWithoutSelector](interfaces/AppCellPropsWithoutSelector.md) +- [AppCellPropsWithSelector](interfaces/AppCellPropsWithSelector.md) +- [AppColumnHelper](interfaces/AppColumnHelper.md) +- [AppHeaderComponent](interfaces/AppHeaderComponent.md) +- [AppHeaderContext](interfaces/AppHeaderContext.md) +- [AppHeaderPropsWithoutSelector](interfaces/AppHeaderPropsWithoutSelector.md) +- [AppHeaderPropsWithSelector](interfaces/AppHeaderPropsWithSelector.md) +- [AppTableComponent](interfaces/AppTableComponent.md) +- [AppTablePropsWithoutSelector](interfaces/AppTablePropsWithoutSelector.md) +- [AppTablePropsWithSelector](interfaces/AppTablePropsWithSelector.md) +- [CreateTableHookResult](interfaces/CreateTableHookResult.md) +- [SubscribeComponent](interfaces/SubscribeComponent.md) +- [SubscribePropsWithSourceIdentity](interfaces/SubscribePropsWithSourceIdentity.md) +- [SubscribePropsWithSourceWithSelector](interfaces/SubscribePropsWithSourceWithSelector.md) +- [SubscribePropsWithStore](interfaces/SubscribePropsWithStore.md) +- [TableHookContexts](interfaces/TableHookContexts.md) + +## Type Aliases + +- [AppColumnDefBase](type-aliases/AppColumnDefBase.md) +- [AppColumnDefTemplate](type-aliases/AppColumnDefTemplate.md) +- [AppDisplayColumnDef](type-aliases/AppDisplayColumnDef.md) +- [AppGroupColumnDef](type-aliases/AppGroupColumnDef.md) +- [AppOctaneTable](type-aliases/AppOctaneTable.md) +- [CreateTableHookOptions](type-aliases/CreateTableHookOptions.md) +- [FlexRenderProps](type-aliases/FlexRenderProps.md) +- [OctaneTable](type-aliases/OctaneTable.md) +- [Renderable](type-aliases/Renderable.md) +- [SubscribeProps](type-aliases/SubscribeProps.md) +- [SubscribePropsWithSource](type-aliases/SubscribePropsWithSource.md) +- [SubscribeSource](type-aliases/SubscribeSource.md) +- [SubscribeStaticChild](type-aliases/SubscribeStaticChild.md) +- [TableComponentType](type-aliases/TableComponentType.md) + +## Variables + +- [Subscribe](variables/Subscribe.md) + +## Functions + +- [createTableHook](functions/createTableHook.md) +- [createTableHookContexts](functions/createTableHookContexts.md) +- [flexRender](functions/flexRender.md) +- [FlexRender](functions/FlexRender-1.md) +- [useTable](functions/useTable.md) diff --git a/docs/framework/octane/reference/interfaces/AppCellComponent.md b/docs/framework/octane/reference/interfaces/AppCellComponent.md new file mode 100644 index 0000000000..867aa1c71d --- /dev/null +++ b/docs/framework/octane/reference/interfaces/AppCellComponent.md @@ -0,0 +1,83 @@ +--- +id: AppCellComponent +title: AppCellComponent +--- + +# Interface: AppCellComponent()\ + +Defined in: [types.ts:674](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L674) + +Component type for AppCell — wraps a cell and provides cell context with +optional Subscribe. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +## Call Signature + +```ts +AppCellComponent(props): unknown; +``` + +Defined in: [types.ts:679](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L679) + +Component type for AppCell — wraps a cell and provides cell context with +optional Subscribe. + +### Type Parameters + +#### TValue + +`TValue` *extends* `unknown` = `unknown` + +### Parameters + +#### props + +[`AppCellPropsWithoutSelector`](AppCellPropsWithoutSelector.md)\<`TFeatures`, `TData`, `TValue`, `TCellComponents`\> + +### Returns + +`unknown` + +## Call Signature + +```ts +AppCellComponent(props): unknown; +``` + +Defined in: [types.ts:687](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L687) + +Component type for AppCell — wraps a cell and provides cell context with +optional Subscribe. + +### Type Parameters + +#### TValue + +`TValue` *extends* `unknown` = `unknown` + +#### TSelected + +`TSelected` = `unknown` + +### Parameters + +#### props + +[`AppCellPropsWithSelector`](AppCellPropsWithSelector.md)\<`TFeatures`, `TData`, `TValue`, `TCellComponents`, `TSelected`\> + +### Returns + +`unknown` diff --git a/docs/framework/octane/reference/interfaces/AppCellContext.md b/docs/framework/octane/reference/interfaces/AppCellContext.md new file mode 100644 index 0000000000..9b4b3cca46 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/AppCellContext.md @@ -0,0 +1,101 @@ +--- +id: AppCellContext +title: AppCellContext +--- + +# Interface: AppCellContext\ + +Defined in: [types.ts:344](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L344) + +Enhanced CellContext with pre-bound cell components. +The `cell` property includes the registered cellComponents. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +## Properties + +### cell + +```ts +cell: Cell_Core & ExtractFeatureMapTypes & TCellComponents & object; +``` + +Defined in: [types.ts:350](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L350) + +#### Type Declaration + +##### FlexRender() + +```ts +FlexRender: () => unknown; +``` + +###### Returns + +`unknown` + +*** + +### column + +```ts +column: Column; +``` + +Defined in: [types.ts:352](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L352) + +*** + +### getValue + +```ts +getValue: Getter; +``` + +Defined in: [types.ts:353](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L353) + +*** + +### renderValue + +```ts +renderValue: Getter; +``` + +Defined in: [types.ts:354](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L354) + +*** + +### row + +```ts +row: Row; +``` + +Defined in: [types.ts:355](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L355) + +*** + +### table + +```ts +table: Table; +``` + +Defined in: [types.ts:356](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L356) diff --git a/docs/framework/octane/reference/interfaces/AppCellPropsWithSelector.md b/docs/framework/octane/reference/interfaces/AppCellPropsWithSelector.md new file mode 100644 index 0000000000..4165f43f77 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/AppCellPropsWithSelector.md @@ -0,0 +1,86 @@ +--- +id: AppCellPropsWithSelector +title: AppCellPropsWithSelector +--- + +# Interface: AppCellPropsWithSelector\ + +Defined in: [types.ts:622](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L622) + +Props for AppCell component — with selector. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +### TSelected + +`TSelected` + +## Properties + +### cell + +```ts +cell: Cell; +``` + +Defined in: [types.ts:629](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L629) + +*** + +### children() + +```ts +children: (cell, state) => unknown; +``` + +Defined in: [types.ts:630](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L630) + +#### Parameters + +##### cell + +`Cell_Core`\<`TFeatures`, `TData`, `TValue`\> & `ExtractFeatureMapTypes`\<`TFeatures`, `Cell_FeatureMap`\> & `TCellComponents` & `object` + +##### state + +`TSelected` + +#### Returns + +`unknown` + +*** + +### selector() + +```ts +selector: (state) => TSelected; +``` + +Defined in: [types.ts:635](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L635) + +#### Parameters + +##### state + +`TableState`\<`TFeatures`\> + +#### Returns + +`TSelected` diff --git a/docs/framework/octane/reference/interfaces/AppCellPropsWithoutSelector.md b/docs/framework/octane/reference/interfaces/AppCellPropsWithoutSelector.md new file mode 100644 index 0000000000..98905b2ef1 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/AppCellPropsWithoutSelector.md @@ -0,0 +1,68 @@ +--- +id: AppCellPropsWithoutSelector +title: AppCellPropsWithoutSelector +--- + +# Interface: AppCellPropsWithoutSelector\ + +Defined in: [types.ts:607](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L607) + +Props for AppCell component — without selector. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +## Properties + +### cell + +```ts +cell: Cell; +``` + +Defined in: [types.ts:613](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L613) + +*** + +### children() + +```ts +children: (cell) => unknown; +``` + +Defined in: [types.ts:614](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L614) + +#### Parameters + +##### cell + +`Cell_Core`\<`TFeatures`, `TData`, `TValue`\> & `ExtractFeatureMapTypes`\<`TFeatures`, `Cell_FeatureMap`\> & `TCellComponents` & `object` + +#### Returns + +`unknown` + +*** + +### selector? + +```ts +optional selector: undefined; +``` + +Defined in: [types.ts:618](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L618) diff --git a/docs/framework/octane/reference/interfaces/AppColumnHelper.md b/docs/framework/octane/reference/interfaces/AppColumnHelper.md new file mode 100644 index 0000000000..5e8780253d --- /dev/null +++ b/docs/framework/octane/reference/interfaces/AppColumnHelper.md @@ -0,0 +1,142 @@ +--- +id: AppColumnHelper +title: AppColumnHelper +--- + +# Interface: AppColumnHelper\ + +Defined in: [types.ts:459](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L459) + +Enhanced column helper with pre-bound components in cell/header/footer +contexts. This enables TypeScript to know about the registered components +when defining columns. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +## Properties + +### accessor() + +```ts +accessor: (accessor, column) => TAccessor extends AccessorFn ? AccessorFnColumnDef : AccessorKeyColumnDef; +``` + +Defined in: [types.ts:469](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L469) + +Creates a data column definition with an accessor key or function. +The cell, header, and footer contexts include pre-bound components. + +#### Type Parameters + +##### TAccessor + +`TAccessor` *extends* `string` \| `AccessorFn`\<`TData`\> + +##### TValue + +`TValue` *extends* `unknown` + +#### Parameters + +##### accessor + +`TAccessor` + +##### column + +`TAccessor` *extends* `AccessorFn`\<`TData`\> ? `Omit`\<`IdentifiedColumnDef`\<`TFeatures`, `TData`, `TValue`\>, `"footer"` \| `"header"` \| `"cell"`\> & `object` & `object` : [`AppColumnDefBase`](../type-aliases/AppColumnDefBase.md)\<`TFeatures`, `TData`, `TValue`, `TCellComponents`, `THeaderComponents`\> + +#### Returns + +`TAccessor` *extends* `AccessorFn`\<`TData`\> ? `AccessorFnColumnDef`\<`TFeatures`, `TData`, `TValue`\> : `AccessorKeyColumnDef`\<`TFeatures`, `TData`, `TValue`\> + +*** + +### columns() + +```ts +columns: (columns) => ColumnDef[] & [...TColumns[]]; +``` + +Defined in: [types.ts:503](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L503) + +Wraps an array of column definitions to preserve each column's individual +TValue type. + +#### Type Parameters + +##### TColumns + +`TColumns` *extends* readonly `ColumnDef`\<`TFeatures`, `TData`, `any`\>[] + +#### Parameters + +##### columns + +\[`...TColumns[]`\] + +#### Returns + +`ColumnDef`\<`TFeatures`, `TData`, `any`\>[] & \[`...TColumns[]`\] + +*** + +### display() + +```ts +display: (column) => DisplayColumnDef; +``` + +Defined in: [types.ts:511](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L511) + +Creates a display column definition for non-data columns. +The cell, header, and footer contexts include pre-bound components. + +#### Parameters + +##### column + +[`AppDisplayColumnDef`](../type-aliases/AppDisplayColumnDef.md)\<`TFeatures`, `TData`, `TCellComponents`, `THeaderComponents`\> + +#### Returns + +`DisplayColumnDef`\<`TFeatures`, `TData`, `unknown`\> + +*** + +### group() + +```ts +group: (column) => GroupColumnDef; +``` + +Defined in: [types.ts:524](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L524) + +Creates a group column definition with nested child columns. +The cell, header, and footer contexts include pre-bound components. + +#### Parameters + +##### column + +[`AppGroupColumnDef`](../type-aliases/AppGroupColumnDef.md)\<`TFeatures`, `TData`, `TCellComponents`, `THeaderComponents`\> + +#### Returns + +`GroupColumnDef`\<`TFeatures`, `TData`, `unknown`\> diff --git a/docs/framework/octane/reference/interfaces/AppHeaderComponent.md b/docs/framework/octane/reference/interfaces/AppHeaderComponent.md new file mode 100644 index 0000000000..fd727fc80b --- /dev/null +++ b/docs/framework/octane/reference/interfaces/AppHeaderComponent.md @@ -0,0 +1,83 @@ +--- +id: AppHeaderComponent +title: AppHeaderComponent +--- + +# Interface: AppHeaderComponent()\ + +Defined in: [types.ts:702](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L702) + +Component type for AppHeader/AppFooter — wraps a header and provides header +context with optional Subscribe. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +## Call Signature + +```ts +AppHeaderComponent(props): unknown; +``` + +Defined in: [types.ts:707](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L707) + +Component type for AppHeader/AppFooter — wraps a header and provides header +context with optional Subscribe. + +### Type Parameters + +#### TValue + +`TValue` *extends* `unknown` = `unknown` + +### Parameters + +#### props + +[`AppHeaderPropsWithoutSelector`](AppHeaderPropsWithoutSelector.md)\<`TFeatures`, `TData`, `TValue`, `THeaderComponents`\> + +### Returns + +`unknown` + +## Call Signature + +```ts +AppHeaderComponent(props): unknown; +``` + +Defined in: [types.ts:715](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L715) + +Component type for AppHeader/AppFooter — wraps a header and provides header +context with optional Subscribe. + +### Type Parameters + +#### TValue + +`TValue` *extends* `unknown` = `unknown` + +#### TSelected + +`TSelected` = `unknown` + +### Parameters + +#### props + +[`AppHeaderPropsWithSelector`](AppHeaderPropsWithSelector.md)\<`TFeatures`, `TData`, `TValue`, `THeaderComponents`, `TSelected`\> + +### Returns + +`unknown` diff --git a/docs/framework/octane/reference/interfaces/AppHeaderContext.md b/docs/framework/octane/reference/interfaces/AppHeaderContext.md new file mode 100644 index 0000000000..63dd2e9fef --- /dev/null +++ b/docs/framework/octane/reference/interfaces/AppHeaderContext.md @@ -0,0 +1,71 @@ +--- +id: AppHeaderContext +title: AppHeaderContext +--- + +# Interface: AppHeaderContext\ + +Defined in: [types.ts:363](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L363) + +Enhanced HeaderContext with pre-bound header components. +The `header` property includes the registered headerComponents. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +## Properties + +### column + +```ts +column: Column; +``` + +Defined in: [types.ts:369](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L369) + +*** + +### header + +```ts +header: Header_Core & ExtractFeatureMapTypes & THeaderComponents & object; +``` + +Defined in: [types.ts:370](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L370) + +#### Type Declaration + +##### FlexRender() + +```ts +FlexRender: () => unknown; +``` + +###### Returns + +`unknown` + +*** + +### table + +```ts +table: Table; +``` + +Defined in: [types.ts:372](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L372) diff --git a/docs/framework/octane/reference/interfaces/AppHeaderPropsWithSelector.md b/docs/framework/octane/reference/interfaces/AppHeaderPropsWithSelector.md new file mode 100644 index 0000000000..958e59b1ed --- /dev/null +++ b/docs/framework/octane/reference/interfaces/AppHeaderPropsWithSelector.md @@ -0,0 +1,86 @@ +--- +id: AppHeaderPropsWithSelector +title: AppHeaderPropsWithSelector +--- + +# Interface: AppHeaderPropsWithSelector\ + +Defined in: [types.ts:654](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L654) + +Props for AppHeader/AppFooter component — with selector. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +### TSelected + +`TSelected` + +## Properties + +### children() + +```ts +children: (header, state) => unknown; +``` + +Defined in: [types.ts:662](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L662) + +#### Parameters + +##### header + +`Header_Core`\<`TFeatures`, `TData`, `TValue`\> & `ExtractFeatureMapTypes`\<`TFeatures`, `Header_FeatureMap`\> & `THeaderComponents` & `object` + +##### state + +`TSelected` + +#### Returns + +`unknown` + +*** + +### header + +```ts +header: Header; +``` + +Defined in: [types.ts:661](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L661) + +*** + +### selector() + +```ts +selector: (state) => TSelected; +``` + +Defined in: [types.ts:667](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L667) + +#### Parameters + +##### state + +`TableState`\<`TFeatures`\> + +#### Returns + +`TSelected` diff --git a/docs/framework/octane/reference/interfaces/AppHeaderPropsWithoutSelector.md b/docs/framework/octane/reference/interfaces/AppHeaderPropsWithoutSelector.md new file mode 100644 index 0000000000..f27bf5c500 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/AppHeaderPropsWithoutSelector.md @@ -0,0 +1,68 @@ +--- +id: AppHeaderPropsWithoutSelector +title: AppHeaderPropsWithoutSelector +--- + +# Interface: AppHeaderPropsWithoutSelector\ + +Defined in: [types.ts:639](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L639) + +Props for AppHeader/AppFooter component — without selector. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +## Properties + +### children() + +```ts +children: (header) => unknown; +``` + +Defined in: [types.ts:646](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L646) + +#### Parameters + +##### header + +`Header_Core`\<`TFeatures`, `TData`, `TValue`\> & `ExtractFeatureMapTypes`\<`TFeatures`, `Header_FeatureMap`\> & `THeaderComponents` & `object` + +#### Returns + +`unknown` + +*** + +### header + +```ts +header: Header; +``` + +Defined in: [types.ts:645](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L645) + +*** + +### selector? + +```ts +optional selector: undefined; +``` + +Defined in: [types.ts:650](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L650) diff --git a/docs/framework/octane/reference/interfaces/AppTableComponent.md b/docs/framework/octane/reference/interfaces/AppTableComponent.md new file mode 100644 index 0000000000..328056697f --- /dev/null +++ b/docs/framework/octane/reference/interfaces/AppTableComponent.md @@ -0,0 +1,62 @@ +--- +id: AppTableComponent +title: AppTableComponent +--- + +# Interface: AppTableComponent()\ + +Defined in: [types.ts:727](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L727) + +Component type for AppTable — root wrapper with optional Subscribe. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +## Call Signature + +```ts +AppTableComponent(props): unknown; +``` + +Defined in: [types.ts:728](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L728) + +Component type for AppTable — root wrapper with optional Subscribe. + +### Parameters + +#### props + +[`AppTablePropsWithoutSelector`](AppTablePropsWithoutSelector.md) + +### Returns + +`unknown` + +## Call Signature + +```ts +AppTableComponent(props): unknown; +``` + +Defined in: [types.ts:729](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L729) + +Component type for AppTable — root wrapper with optional Subscribe. + +### Type Parameters + +#### TSelected + +`TSelected` + +### Parameters + +#### props + +[`AppTablePropsWithSelector`](AppTablePropsWithSelector.md)\<`TFeatures`, `TSelected`\> + +### Returns + +`unknown` diff --git a/docs/framework/octane/reference/interfaces/AppTablePropsWithSelector.md b/docs/framework/octane/reference/interfaces/AppTablePropsWithSelector.md new file mode 100644 index 0000000000..e1bf98d931 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/AppTablePropsWithSelector.md @@ -0,0 +1,60 @@ +--- +id: AppTablePropsWithSelector +title: AppTablePropsWithSelector +--- + +# Interface: AppTablePropsWithSelector\ + +Defined in: [types.ts:598](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L598) + +Props for AppTable component — with selector. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TSelected + +`TSelected` + +## Properties + +### children() + +```ts +children: (state) => unknown; +``` + +Defined in: [types.ts:602](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L602) + +#### Parameters + +##### state + +`TSelected` + +#### Returns + +`unknown` + +*** + +### selector() + +```ts +selector: (state) => TSelected; +``` + +Defined in: [types.ts:603](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L603) + +#### Parameters + +##### state + +`TableState`\<`TFeatures`\> + +#### Returns + +`TSelected` diff --git a/docs/framework/octane/reference/interfaces/AppTablePropsWithoutSelector.md b/docs/framework/octane/reference/interfaces/AppTablePropsWithoutSelector.md new file mode 100644 index 0000000000..0e6e11a8e1 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/AppTablePropsWithoutSelector.md @@ -0,0 +1,30 @@ +--- +id: AppTablePropsWithoutSelector +title: AppTablePropsWithoutSelector +--- + +# Interface: AppTablePropsWithoutSelector + +Defined in: [types.ts:592](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L592) + +Props for AppTable component — without selector. + +## Properties + +### children + +```ts +children: unknown; +``` + +Defined in: [types.ts:593](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L593) + +*** + +### selector? + +```ts +optional selector: undefined; +``` + +Defined in: [types.ts:594](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L594) diff --git a/docs/framework/octane/reference/interfaces/CreateTableHookResult.md b/docs/framework/octane/reference/interfaces/CreateTableHookResult.md new file mode 100644 index 0000000000..4ede6ee0b4 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/CreateTableHookResult.md @@ -0,0 +1,179 @@ +--- +id: CreateTableHookResult +title: CreateTableHookResult +--- + +# Interface: CreateTableHookResult\ + +Defined in: [types.ts:811](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L811) + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TTableComponents + +`TTableComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`TableComponentType`](../type-aliases/TableComponentType.md)\> + +## Properties + +### appFeatures + +```ts +appFeatures: TFeatures; +``` + +Defined in: [types.ts:818](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L818) + +The features object that was passed to `createTableHook`. + +*** + +### createAppColumnHelper() + +```ts +createAppColumnHelper: () => AppColumnHelper; +``` + +Defined in: [types.ts:823](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L823) + +A column helper pre-bound to `TFeatures` and the registered components, so +the cell/header/footer render props expose the bound components. + +#### Type Parameters + +##### TData + +`TData` *extends* `RowData` + +#### Returns + +[`AppColumnHelper`](AppColumnHelper.md)\<`TFeatures`, `TData`, `TCellComponents`, `THeaderComponents`\> + +*** + +### useAppTable() + +```ts +useAppTable: (tableOptions, selector?) => AppOctaneTable; +``` + +Defined in: [types.ts:833](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L833) + +Creates a table with the `App*` wrapper components and registered +`tableComponents` attached. `TData` is inferred from the `data` option. + +#### Type Parameters + +##### TData + +`TData` *extends* `RowData` + +##### TSelected + +`TSelected` = `TableState`\<`TFeatures`\> + +#### Parameters + +##### tableOptions + +`Omit`\<`TableOptions`\<`TFeatures`, `TData`\>, `"features"`\> + +##### selector? + +(`state`) => `TSelected` + +#### Returns + +[`AppOctaneTable`](../type-aliases/AppOctaneTable.md)\<`TFeatures`, `TData`, `TSelected`, `TTableComponents`, `TCellComponents`, `THeaderComponents`\> + +*** + +### useCellContext() + +```ts +useCellContext: () => Cell_Core & ExtractFeatureMapTypes & TCellComponents & object; +``` + +Defined in: [types.ts:870](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L870) + +Reads the cell provided by the nearest ``, extended with +your `cellComponents` and a context-bound `FlexRender`. + +#### Type Parameters + +##### TValue + +`TValue` *extends* `unknown` = `unknown` + +#### Returns + +`Cell_Core`\<`TFeatures`, `any`, `TValue`\> & `ExtractFeatureMapTypes`\<`TFeatures`, `Cell_FeatureMap`\> & `TCellComponents` & `object` + +*** + +### useHeaderContext() + +```ts +useHeaderContext: () => Header_Core & ExtractFeatureMapTypes & THeaderComponents & object; +``` + +Defined in: [types.ts:881](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L881) + +Reads the header provided by the nearest `` / +``, extended with your `headerComponents` and a +context-bound `FlexRender`. + +#### Type Parameters + +##### TValue + +`TValue` *extends* `unknown` = `unknown` + +#### Returns + +`Header_Core`\<`TFeatures`, `any`, `TValue`\> & `ExtractFeatureMapTypes`\<`TFeatures`, `Header_FeatureMap`\> & `THeaderComponents` & `object` + +*** + +### useTableContext() + +```ts +useTableContext: () => AppOctaneTable; +``` + +Defined in: [types.ts:855](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L855) + +Reads the table provided by the nearest ``. This is the +same extended instance `useAppTable` returns, so the `App*` components and +your `tableComponents` are available on it. + +Pass `TSelected` to match the selector you gave `useAppTable`, so +`table.state` is typed as the selected slice. It cannot be inferred +automatically (context does not carry the provider's generics), so it +defaults to the full table state, which is correct for the common case of +`useAppTable` without a selector. + +#### Type Parameters + +##### TData + +`TData` *extends* `RowData` = `RowData` + +##### TSelected + +`TSelected` = `TableState`\<`TFeatures`\> + +#### Returns + +[`AppOctaneTable`](../type-aliases/AppOctaneTable.md)\<`TFeatures`, `TData`, `TSelected`, `TTableComponents`, `TCellComponents`, `THeaderComponents`\> diff --git a/docs/framework/octane/reference/interfaces/SubscribeComponent.md b/docs/framework/octane/reference/interfaces/SubscribeComponent.md new file mode 100644 index 0000000000..738027f0f2 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/SubscribeComponent.md @@ -0,0 +1,132 @@ +--- +id: SubscribeComponent +title: SubscribeComponent +--- + +# Interface: SubscribeComponent() + +Defined in: [types.ts:184](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L184) + +The call signature of the standalone `Subscribe` component. + +Declared as an overloaded callable type rather than `function` overloads +because the implementation is authored in `.tsrx`: a `@{ … }` body cannot +carry preceding overload signatures (TS2384). Overload *resolution* is what +matters at the JSX call site, and a callable type provides exactly that. + +The **source** overloads come first so `TSourceValue` is inferred from +`source`; the identity overload (no `selector`) is separate so children +receive `TSourceValue` rather than `unknown`. + +## Call Signature + +```ts +SubscribeComponent(props): unknown; +``` + +Defined in: [types.ts:185](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L185) + +The call signature of the standalone `Subscribe` component. + +Declared as an overloaded callable type rather than `function` overloads +because the implementation is authored in `.tsrx`: a `@{ … }` body cannot +carry preceding overload signatures (TS2384). Overload *resolution* is what +matters at the JSX call site, and a callable type provides exactly that. + +The **source** overloads come first so `TSourceValue` is inferred from +`source`; the identity overload (no `selector`) is separate so children +receive `TSourceValue` rather than `unknown`. + +### Type Parameters + +#### TSourceValue + +`TSourceValue` + +### Parameters + +#### props + +[`SubscribePropsWithSourceIdentity`](SubscribePropsWithSourceIdentity.md)\<`TSourceValue`\> + +### Returns + +`unknown` + +## Call Signature + +```ts +SubscribeComponent(props): unknown; +``` + +Defined in: [types.ts:188](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L188) + +The call signature of the standalone `Subscribe` component. + +Declared as an overloaded callable type rather than `function` overloads +because the implementation is authored in `.tsrx`: a `@{ … }` body cannot +carry preceding overload signatures (TS2384). Overload *resolution* is what +matters at the JSX call site, and a callable type provides exactly that. + +The **source** overloads come first so `TSourceValue` is inferred from +`source`; the identity overload (no `selector`) is separate so children +receive `TSourceValue` rather than `unknown`. + +### Type Parameters + +#### TSourceValue + +`TSourceValue` + +#### TSelected + +`TSelected` + +### Parameters + +#### props + +[`SubscribePropsWithSourceWithSelector`](SubscribePropsWithSourceWithSelector.md)\<`TSourceValue`, `TSelected`\> + +### Returns + +`unknown` + +## Call Signature + +```ts +SubscribeComponent(props): unknown; +``` + +Defined in: [types.ts:191](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L191) + +The call signature of the standalone `Subscribe` component. + +Declared as an overloaded callable type rather than `function` overloads +because the implementation is authored in `.tsrx`: a `@{ … }` body cannot +carry preceding overload signatures (TS2384). Overload *resolution* is what +matters at the JSX call site, and a callable type provides exactly that. + +The **source** overloads come first so `TSourceValue` is inferred from +`source`; the identity overload (no `selector`) is separate so children +receive `TSourceValue` rather than `unknown`. + +### Type Parameters + +#### TFeatures + +`TFeatures` *extends* `TableFeatures` + +#### TSelected + +`TSelected` + +### Parameters + +#### props + +[`SubscribePropsWithStore`](SubscribePropsWithStore.md)\<`TFeatures`, `TSelected`\> + +### Returns + +`unknown` diff --git a/docs/framework/octane/reference/interfaces/SubscribePropsWithSourceIdentity.md b/docs/framework/octane/reference/interfaces/SubscribePropsWithSourceIdentity.md new file mode 100644 index 0000000000..0be5e0c3bf --- /dev/null +++ b/docs/framework/octane/reference/interfaces/SubscribePropsWithSourceIdentity.md @@ -0,0 +1,50 @@ +--- +id: SubscribePropsWithSourceIdentity +title: SubscribePropsWithSourceIdentity +--- + +# Interface: SubscribePropsWithSourceIdentity\ + +Defined in: [types.ts:137](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L137) + +Subscribe to the full value of a source (e.g. `table.atoms.rowSelection` or +`table.optionsStore`). Omitting `selector` is equivalent to the identity +selector — children receive `TSourceValue`. + +## Type Parameters + +### TSourceValue + +`TSourceValue` + +## Properties + +### children + +```ts +children: + | SubscribeStaticChild + | (state) => unknown; +``` + +Defined in: [types.ts:140](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L140) + +*** + +### selector? + +```ts +optional selector: undefined; +``` + +Defined in: [types.ts:139](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L139) + +*** + +### source + +```ts +source: SubscribeSource; +``` + +Defined in: [types.ts:138](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L138) diff --git a/docs/framework/octane/reference/interfaces/SubscribePropsWithSourceWithSelector.md b/docs/framework/octane/reference/interfaces/SubscribePropsWithSourceWithSelector.md new file mode 100644 index 0000000000..df9f148774 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/SubscribePropsWithSourceWithSelector.md @@ -0,0 +1,63 @@ +--- +id: SubscribePropsWithSourceWithSelector +title: SubscribePropsWithSourceWithSelector +--- + +# Interface: SubscribePropsWithSourceWithSelector\ + +Defined in: [types.ts:147](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L147) + +Subscribe to a projected value from a source (atom or store). The selector +receives the source value; children receive the projected `TSelected`. + +## Type Parameters + +### TSourceValue + +`TSourceValue` + +### TSelected + +`TSelected` + +## Properties + +### children + +```ts +children: + | SubscribeStaticChild + | (state) => unknown; +``` + +Defined in: [types.ts:150](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L150) + +*** + +### selector() + +```ts +selector: (state) => TSelected; +``` + +Defined in: [types.ts:149](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L149) + +#### Parameters + +##### state + +`TSourceValue` + +#### Returns + +`TSelected` + +*** + +### source + +```ts +source: SubscribeSource; +``` + +Defined in: [types.ts:148](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L148) diff --git a/docs/framework/octane/reference/interfaces/SubscribePropsWithStore.md b/docs/framework/octane/reference/interfaces/SubscribePropsWithStore.md new file mode 100644 index 0000000000..ab6926c8b5 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/SubscribePropsWithStore.md @@ -0,0 +1,69 @@ +--- +id: SubscribePropsWithStore +title: SubscribePropsWithStore +--- + +# Interface: SubscribePropsWithStore\ + +Defined in: [types.ts:116](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L116) + +Subscribe to `table.store` (full table state). The selector receives the full +TableState. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TSelected + +`TSelected` + +## Properties + +### children + +```ts +children: + | SubscribeStaticChild + | (state) => unknown; +``` + +Defined in: [types.ts:129](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L129) + +*** + +### selector() + +```ts +selector: (state) => TSelected; +``` + +Defined in: [types.ts:128](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L128) + +Select from full table state. Re-renders when the selected value changes +(shallow compare). + +Required in store mode so you never accidentally subscribe to the whole +store without an explicit projection. + +#### Parameters + +##### state + +`TableState`\<`TFeatures`\> + +#### Returns + +`TSelected` + +*** + +### source + +```ts +source: SubscribeSource>; +``` + +Defined in: [types.ts:120](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L120) diff --git a/docs/framework/octane/reference/interfaces/TableHookContexts.md b/docs/framework/octane/reference/interfaces/TableHookContexts.md new file mode 100644 index 0000000000..19e2f493c0 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/TableHookContexts.md @@ -0,0 +1,111 @@ +--- +id: TableHookContexts +title: TableHookContexts +--- + +# Interface: TableHookContexts\ + +Defined in: [types.ts:313](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L313) + +The object returned by `createTableHookContexts`: three scoped octane +contexts plus matching context hooks. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +## Properties + +### cellContext + +```ts +cellContext: Context>; +``` + +Defined in: [types.ts:318](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L318) + +*** + +### headerContext + +```ts +headerContext: Context>; +``` + +Defined in: [types.ts:319](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L319) + +*** + +### tableContext + +```ts +tableContext: Context>; +``` + +Defined in: [types.ts:317](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L317) + +*** + +### useCellContext() + +```ts +useCellContext: () => Cell; +``` + +Defined in: [types.ts:324](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L324) + +#### Type Parameters + +##### TValue + +`TValue` *extends* `unknown` = `unknown` + +#### Returns + +`Cell`\<`TFeatures`, `any`, `TValue`\> + +*** + +### useHeaderContext() + +```ts +useHeaderContext: () => Header; +``` + +Defined in: [types.ts:329](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L329) + +#### Type Parameters + +##### TValue + +`TValue` *extends* `unknown` = `unknown` + +#### Returns + +`Header`\<`TFeatures`, `any`, `TValue`\> + +*** + +### useTableContext() + +```ts +useTableContext: () => OctaneTable; +``` + +Defined in: [types.ts:320](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L320) + +#### Type Parameters + +##### TTableData + +`TTableData` *extends* `RowData` = `TData` + +#### Returns + +[`OctaneTable`](../type-aliases/OctaneTable.md)\<`TFeatures`, `TTableData`\> diff --git a/docs/framework/octane/reference/type-aliases/AppColumnDefBase.md b/docs/framework/octane/reference/type-aliases/AppColumnDefBase.md new file mode 100644 index 0000000000..e33ce2b855 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/AppColumnDefBase.md @@ -0,0 +1,57 @@ +--- +id: AppColumnDefBase +title: AppColumnDefBase +--- + +# Type Alias: AppColumnDefBase\ + +```ts +type AppColumnDefBase = Omit, "cell" | "header" | "footer"> & object; +``` + +Defined in: [types.ts:386](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L386) + +Enhanced column definition base with pre-bound components in +cell/header/footer contexts. + +## Type Declaration + +### cell? + +```ts +optional cell: AppColumnDefTemplate>; +``` + +### footer? + +```ts +optional footer: AppColumnDefTemplate>; +``` + +### header? + +```ts +optional header: AppColumnDefTemplate>; +``` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`TableComponentType`](TableComponentType.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`TableComponentType`](TableComponentType.md)\> diff --git a/docs/framework/octane/reference/type-aliases/AppColumnDefTemplate.md b/docs/framework/octane/reference/type-aliases/AppColumnDefTemplate.md new file mode 100644 index 0000000000..352d0596ed --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/AppColumnDefTemplate.md @@ -0,0 +1,20 @@ +--- +id: AppColumnDefTemplate +title: AppColumnDefTemplate +--- + +# Type Alias: AppColumnDefTemplate\ + +```ts +type AppColumnDefTemplate = string | (props) => any; +``` + +Defined in: [types.ts:378](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L378) + +Template type for column definitions that can be a string or a function. + +## Type Parameters + +### TProps + +`TProps` *extends* `object` diff --git a/docs/framework/octane/reference/type-aliases/AppDisplayColumnDef.md b/docs/framework/octane/reference/type-aliases/AppDisplayColumnDef.md new file mode 100644 index 0000000000..3250964147 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/AppDisplayColumnDef.md @@ -0,0 +1,52 @@ +--- +id: AppDisplayColumnDef +title: AppDisplayColumnDef +--- + +# Type Alias: AppDisplayColumnDef\ + +```ts +type AppDisplayColumnDef = Omit, "cell" | "header" | "footer"> & object; +``` + +Defined in: [types.ts:410](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L410) + +Enhanced display column definition with pre-bound components. + +## Type Declaration + +### cell? + +```ts +optional cell: AppColumnDefTemplate>; +``` + +### footer? + +```ts +optional footer: AppColumnDefTemplate>; +``` + +### header? + +```ts +optional header: AppColumnDefTemplate>; +``` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`TableComponentType`](TableComponentType.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`TableComponentType`](TableComponentType.md)\> diff --git a/docs/framework/octane/reference/type-aliases/AppGroupColumnDef.md b/docs/framework/octane/reference/type-aliases/AppGroupColumnDef.md new file mode 100644 index 0000000000..b6b6ed7b95 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/AppGroupColumnDef.md @@ -0,0 +1,58 @@ +--- +id: AppGroupColumnDef +title: AppGroupColumnDef +--- + +# Type Alias: AppGroupColumnDef\ + +```ts +type AppGroupColumnDef = Omit, "cell" | "header" | "footer" | "columns"> & object; +``` + +Defined in: [types.ts:433](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L433) + +Enhanced group column definition with pre-bound components. + +## Type Declaration + +### cell? + +```ts +optional cell: AppColumnDefTemplate>; +``` + +### columns? + +```ts +optional columns: ColumnDef[]; +``` + +### footer? + +```ts +optional footer: AppColumnDefTemplate>; +``` + +### header? + +```ts +optional header: AppColumnDefTemplate>; +``` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`TableComponentType`](TableComponentType.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`TableComponentType`](TableComponentType.md)\> diff --git a/docs/framework/octane/reference/type-aliases/AppOctaneTable.md b/docs/framework/octane/reference/type-aliases/AppOctaneTable.md new file mode 100644 index 0000000000..8813bd4f0c --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/AppOctaneTable.md @@ -0,0 +1,117 @@ +--- +id: AppOctaneTable +title: AppOctaneTable +--- + +# Type Alias: AppOctaneTable\ + +```ts +type AppOctaneTable = OctaneTable & CoreNoInfer & object; +``` + +Defined in: [types.ts:737](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L737) + +Extended table API returned by `useAppTable` with all App wrapper components. + +## Type Declaration + +### AppCell + +```ts +AppCell: AppCellComponent>; +``` + +Wraps a cell and provides cell context with pre-bound cellComponents. +Optionally accepts a selector for Subscribe functionality. + +#### Example + +```tsx + + {(c) => } + +``` + +### AppFooter + +```ts +AppFooter: AppHeaderComponent>; +``` + +Wraps a footer and provides header context with pre-bound +headerComponents. + +#### Example + +```tsx + + {(f) => } + +``` + +### AppHeader + +```ts +AppHeader: AppHeaderComponent>; +``` + +Wraps a header and provides header context with pre-bound +headerComponents. Optionally accepts a selector for Subscribe +functionality. + +#### Example + +```tsx + + {(h) => } + +``` + +### AppTable + +```ts +AppTable: AppTableComponent; +``` + +Root wrapper component that provides table context with optional +Subscribe. + +#### Example + +```tsx +// Without selector — children is a renderable + + …
+
+ +// With selector — children receives selected state + s.pagination}> + {(pagination) =>
{pagination.pageIndex as unknown as string}
} +
+``` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TSelected + +`TSelected` + +### TTableComponents + +`TTableComponents` *extends* `Record`\<`string`, [`TableComponentType`](TableComponentType.md)\> + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`TableComponentType`](TableComponentType.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`TableComponentType`](TableComponentType.md)\> diff --git a/docs/framework/octane/reference/type-aliases/CreateTableHookOptions.md b/docs/framework/octane/reference/type-aliases/CreateTableHookOptions.md new file mode 100644 index 0000000000..03108346c5 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/CreateTableHookOptions.md @@ -0,0 +1,122 @@ +--- +id: CreateTableHookOptions +title: CreateTableHookOptions +--- + +# Type Alias: CreateTableHookOptions\ + +```ts +type CreateTableHookOptions = Omit, "columns" | "data" | "store" | "state" | "initialState"> & object; +``` + +Defined in: [types.ts:539](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L539) + +Options for creating a table hook with pre-bound components and default table +options. Extends all TableOptions except +`columns` | `data` | `store` | `state` | `initialState`. + +## Type Declaration + +### cellComponents? + +```ts +optional cellComponents: TCellComponents; +``` + +Cell-level components that need access to the cell instance. +These are available on the cell object passed to AppCell's children. +Use `useCellContext()` inside these components. + +#### Example + +```ts +{ TextCell, NumberCell, DateCell, CurrencyCell } +``` + +### cellContext? + +```ts +optional cellContext: Context>; +``` + +A custom octane context for the cell instance, used inside your +`cellComponents`. + +#### See + +CreateTableHookOptions.tableContext + +### headerComponents? + +```ts +optional headerComponents: THeaderComponents; +``` + +Header-level components that need access to the header instance. +These are available on the header object passed to AppHeader/AppFooter's +children. Use `useHeaderContext()` inside these components. + +#### Example + +```ts +{ SortIndicator, ColumnFilter, ResizeHandle } +``` + +### headerContext? + +```ts +optional headerContext: Context>; +``` + +A custom octane context for the header instance, used inside your +`headerComponents` (and footer components). + +#### See + +CreateTableHookOptions.tableContext + +### tableComponents? + +```ts +optional tableComponents: TTableComponents; +``` + +Table-level components that need access to the table instance. +These are available directly on the table object returned by useAppTable. +Use `useTableContext()` inside these components. + +#### Example + +```ts +{ PaginationControls, GlobalFilter, RowCount } +``` + +### tableContext? + +```ts +optional tableContext: Context>; +``` + +A custom octane context for the table instance (read with `useContext` +inside your `tableComponents`). Optional: defaults to a shared +module-scoped context. Only pass your own (created via `createContext`) +when you need to isolate this table's context from other tables, e.g. when +nesting one table inside another. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TTableComponents + +`TTableComponents` *extends* `Record`\<`string`, [`TableComponentType`](TableComponentType.md)\> + +### TCellComponents + +`TCellComponents` *extends* `Record`\<`string`, [`TableComponentType`](TableComponentType.md)\> + +### THeaderComponents + +`THeaderComponents` *extends* `Record`\<`string`, [`TableComponentType`](TableComponentType.md)\> diff --git a/docs/framework/octane/reference/type-aliases/FlexRenderProps.md b/docs/framework/octane/reference/type-aliases/FlexRenderProps.md new file mode 100644 index 0000000000..2b803fe9b3 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/FlexRenderProps.md @@ -0,0 +1,44 @@ +--- +id: FlexRenderProps +title: FlexRenderProps +--- + +# Type Alias: FlexRenderProps\ + +```ts +type FlexRenderProps = + | { + cell: Cell; + footer?: never; + header?: never; +} + | { + cell?: never; + footer?: never; + header: Header; +} + | { + cell?: never; + footer: Header; + header?: never; +}; +``` + +Defined in: [types.ts:78](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L78) + +Props for the [FlexRender](../functions/FlexRender-1.md) component. Exactly one of `cell`, `header`, +or `footer` may be passed. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TValue + +`TValue` *extends* `CellData` = `CellData` diff --git a/docs/framework/octane/reference/type-aliases/OctaneTable.md b/docs/framework/octane/reference/type-aliases/OctaneTable.md new file mode 100644 index 0000000000..84b9d13c63 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/OctaneTable.md @@ -0,0 +1,219 @@ +--- +id: OctaneTable +title: OctaneTable +--- + +# Type Alias: OctaneTable\ + +```ts +type OctaneTable = Omit, "store"> & object; +``` + +Defined in: [types.ts:205](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L205) + +The table instance returned by [useTable](../functions/useTable.md): the framework-agnostic +`Table` from table-core, plus octane's `Subscribe`/`FlexRender` components +and the selected `state`. + +## Type Declaration + +### FlexRender() + +```ts +FlexRender: (props) => OctaneNode; +``` + +An octane component that renders headers, cells, or footers with custom +markup. Use this utility component instead of manually calling +`flexRender`. + +#### Type Parameters + +##### TValue + +`TValue` *extends* `CellData` = `CellData` + +#### Parameters + +##### props + +[`FlexRenderProps`](FlexRenderProps.md)\<`TFeatures`, `TData`, `TValue`\> + +#### Returns + +`OctaneNode` + +#### Example + +```tsx + + + +``` + +This replaces calling `flexRender` directly like this: +```tsx +flexRender(cell.column.columnDef.cell, cell.getContext()) +flexRender(header.column.columnDef.header, header.getContext()) +flexRender(footer.column.columnDef.footer, footer.getContext()) +``` + +### state + +```ts +readonly state: Readonly; +``` + +The selected state of the table. This state may not match the structure of +the full table state because it is selected by the selector function that +you pass as the 2nd argument to `useTable`. + +#### Example + +```ts +const table = useTable(options, (state) => ({ globalFilter: state.globalFilter })) + +table.state.globalFilter +``` + +### ~~store~~ + +```ts +readonly store: Table["store"]; +``` + +#### Deprecated + +Prefer `table.state` for render reads, +`table.atoms..get()` for slice snapshots, or +`table.Subscribe` / `useSelector(table.store, selector)` for explicit +subscriptions. `table.store.state` is a current-value snapshot and is easy +to misuse in render code. + +### Subscribe() + +```ts +Subscribe: { + (props): unknown; + (props): unknown; + (props): unknown; +}; +``` + +Overloads (not a single union) so `selector` callbacks get correct +contextual types in JSX; a union of two `selector` signatures degrades to +implicit `any`. + +Source **without** `selector` is a separate overload so children receive +`TSourceValue` (identity projection). If `selector` were optional on one +overload, `TSubSelected` would default to `unknown` instead of inferring +from the source. + +The **source** overloads are listed first so `TSourceValue` is inferred +from `source`. + +#### Call Signature + +```ts +(props): unknown; +``` + +##### Type Parameters + +###### TSourceValue + +`TSourceValue` + +##### Parameters + +###### props + +###### children + + \| (`state`) => `OctaneNode` + \| [`SubscribeStaticChild`](SubscribeStaticChild.md) + +###### selector? + +`undefined` + +###### source + +[`SubscribeSource`](SubscribeSource.md)\<`TSourceValue`\> + +##### Returns + +`unknown` + +#### Call Signature + +```ts +(props): unknown; +``` + +##### Type Parameters + +###### TSourceValue + +`TSourceValue` + +###### TSubSelected + +`TSubSelected` + +##### Parameters + +###### props + +###### children + + \| (`state`) => `OctaneNode` + \| [`SubscribeStaticChild`](SubscribeStaticChild.md) + +###### selector + +(`state`) => `TSubSelected` + +###### source + +[`SubscribeSource`](SubscribeSource.md)\<`TSourceValue`\> + +##### Returns + +`unknown` + +#### Call Signature + +```ts +(props): unknown; +``` + +##### Type Parameters + +###### TSubSelected + +`TSubSelected` + +##### Parameters + +###### props + +`Omit`\<[`SubscribePropsWithStore`](../interfaces/SubscribePropsWithStore.md)\<`TFeatures`, `TSubSelected`\>, `"source"`\> + +##### Returns + +`unknown` + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TData + +`TData` *extends* `RowData` + +### TSelected + +`TSelected` = `TableState`\<`TFeatures`\> diff --git a/docs/framework/octane/reference/type-aliases/Renderable.md b/docs/framework/octane/reference/type-aliases/Renderable.md new file mode 100644 index 0000000000..f8ea7c5a4a --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/Renderable.md @@ -0,0 +1,29 @@ +--- +id: Renderable +title: Renderable +--- + +# Type Alias: Renderable\ + +```ts +type Renderable = + | ComponentBody + | ElementDescriptor + | string + | number + | boolean + | null + | undefined; +``` + +Defined in: [types.ts:53](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L53) + +Anything a `columnDef.cell`/`header`/`footer` slot may hold: an octane +component, an already-created element descriptor, or a primitive that +renders as text. + +## Type Parameters + +### TProps + +`TProps` diff --git a/docs/framework/octane/reference/type-aliases/SubscribeProps.md b/docs/framework/octane/reference/type-aliases/SubscribeProps.md new file mode 100644 index 0000000000..387da63c61 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/SubscribeProps.md @@ -0,0 +1,29 @@ +--- +id: SubscribeProps +title: SubscribeProps +--- + +# Type Alias: SubscribeProps\ + +```ts +type SubscribeProps = + | SubscribePropsWithStore + | SubscribePropsWithSourceIdentity +| SubscribePropsWithSourceWithSelector; +``` + +Defined in: [types.ts:163](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L163) + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* `TableFeatures` + +### TSelected + +`TSelected` = `unknown` + +### TSourceValue + +`TSourceValue` = `unknown` diff --git a/docs/framework/octane/reference/type-aliases/SubscribePropsWithSource.md b/docs/framework/octane/reference/type-aliases/SubscribePropsWithSource.md new file mode 100644 index 0000000000..57aaaf3415 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/SubscribePropsWithSource.md @@ -0,0 +1,29 @@ +--- +id: SubscribePropsWithSource +title: SubscribePropsWithSource +--- + +# Type Alias: SubscribePropsWithSource\ + +```ts +type SubscribePropsWithSource = + | SubscribePropsWithSourceIdentity +| SubscribePropsWithSourceWithSelector; +``` + +Defined in: [types.ts:159](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L159) + +Subscribe to a single source — atom or store (identity or projected). Prefer +[SubscribePropsWithSourceIdentity](../interfaces/SubscribePropsWithSourceIdentity.md) or +[SubscribePropsWithSourceWithSelector](../interfaces/SubscribePropsWithSourceWithSelector.md) for clearer inference when +`selector` is omitted. + +## Type Parameters + +### TSourceValue + +`TSourceValue` + +### TSelected + +`TSelected` = `TSourceValue` diff --git a/docs/framework/octane/reference/type-aliases/SubscribeSource.md b/docs/framework/octane/reference/type-aliases/SubscribeSource.md new file mode 100644 index 0000000000..1e3ca4065b --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/SubscribeSource.md @@ -0,0 +1,24 @@ +--- +id: SubscribeSource +title: SubscribeSource +--- + +# Type Alias: SubscribeSource\ + +```ts +type SubscribeSource = + | Atom + | ReadonlyAtom + | Store +| ReadonlyStore; +``` + +Defined in: [types.ts:92](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L92) + +Any atom or store that `Subscribe` can read and subscribe to. + +## Type Parameters + +### TValue + +`TValue` diff --git a/docs/framework/octane/reference/type-aliases/SubscribeStaticChild.md b/docs/framework/octane/reference/type-aliases/SubscribeStaticChild.md new file mode 100644 index 0000000000..0291cb4bc1 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/SubscribeStaticChild.md @@ -0,0 +1,23 @@ +--- +id: SubscribeStaticChild +title: SubscribeStaticChild +--- + +# Type Alias: SubscribeStaticChild + +```ts +type SubscribeStaticChild = + | ElementDescriptor + | string + | number + | boolean + | null + | undefined +| ReadonlyArray; +``` + +Defined in: [types.ts:103](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L103) + +A non-render-prop child accepted by Subscribe. This stays concrete instead +of using `OctaneNode` (currently `unknown`) so function children retain +contextual typing. diff --git a/docs/framework/octane/reference/type-aliases/TableComponentType.md b/docs/framework/octane/reference/type-aliases/TableComponentType.md new file mode 100644 index 0000000000..d9bbe0494d --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/TableComponentType.md @@ -0,0 +1,33 @@ +--- +id: TableComponentType +title: TableComponentType +--- + +# Type Alias: TableComponentType()\ + +```ts +type TableComponentType = (props) => OctaneNode; +``` + +Defined in: [types.ts:68](https://github.com/TanStack/table/blob/main/packages/octane-table/src/types.ts#L68) + +A component in a `createTableHook` component registry. + +Structural on purpose (octane components are plain functions), so registries +accept components declared in `.tsrx`, `.tsx`, or plain `.ts` alike. + +## Type Parameters + +### TProps + +`TProps` = `any` + +## Parameters + +### props + +`TProps` + +## Returns + +`OctaneNode` diff --git a/docs/framework/octane/reference/variables/Subscribe.md b/docs/framework/octane/reference/variables/Subscribe.md new file mode 100644 index 0000000000..69150fb07c --- /dev/null +++ b/docs/framework/octane/reference/variables/Subscribe.md @@ -0,0 +1,12 @@ +--- +id: Subscribe +title: Subscribe +--- + +# Variable: Subscribe + +```ts +const Subscribe: SubscribeComponent; +``` + +Defined in: [Subscribe.tsrx.d.ts:8](https://github.com/TanStack/table/blob/main/packages/octane-table/src/Subscribe.tsrx.d.ts#L8) diff --git a/docs/framework/preact/reference/functions/FlexRender-1.md b/docs/framework/preact/reference/functions/FlexRender-1.md index 706c45da04..7f29a39faf 100644 --- a/docs/framework/preact/reference/functions/FlexRender-1.md +++ b/docs/framework/preact/reference/functions/FlexRender-1.md @@ -9,7 +9,7 @@ title: FlexRender function FlexRender(props): ComponentChild; ``` -Defined in: [FlexRender.tsx:98](https://github.com/TanStack/table/blob/main/packages/preact-table/src/FlexRender.tsx#L98) +Defined in: [FlexRender.tsx:101](https://github.com/TanStack/table/blob/main/packages/preact-table/src/FlexRender.tsx#L101) Simplified component wrapper of `flexRender`. Use this utility component to render headers, cells, or footers with custom markup. Only one prop (`cell`, `header`, or `footer`) may be passed. diff --git a/docs/framework/preact/reference/functions/flexRender.md b/docs/framework/preact/reference/functions/flexRender.md index e4ec898268..3b1cabadbf 100644 --- a/docs/framework/preact/reference/functions/flexRender.md +++ b/docs/framework/preact/reference/functions/flexRender.md @@ -9,7 +9,7 @@ title: flexRender function flexRender(Comp, props): ComponentChild | Element; ``` -Defined in: [FlexRender.tsx:46](https://github.com/TanStack/table/blob/main/packages/preact-table/src/FlexRender.tsx#L46) +Defined in: [FlexRender.tsx:49](https://github.com/TanStack/table/blob/main/packages/preact-table/src/FlexRender.tsx#L49) If rendering headers, cells, or footers with custom markup, use flexRender instead of `cell.getValue()` or `cell.renderValue()`. diff --git a/docs/framework/preact/reference/functions/useTable.md b/docs/framework/preact/reference/functions/useTable.md index 8189a123d8..0390c6ea64 100644 --- a/docs/framework/preact/reference/functions/useTable.md +++ b/docs/framework/preact/reference/functions/useTable.md @@ -9,7 +9,7 @@ title: useTable function useTable(tableOptions, selector?): PreactTable; ``` -Defined in: [useTable.ts:112](https://github.com/TanStack/table/blob/main/packages/preact-table/src/useTable.ts#L112) +Defined in: [useTable.ts:120](https://github.com/TanStack/table/blob/main/packages/preact-table/src/useTable.ts#L120) Creates a Preact table instance backed by TanStack Store atoms. diff --git a/docs/framework/preact/reference/type-aliases/AppGroupColumnDef.md b/docs/framework/preact/reference/type-aliases/AppGroupColumnDef.md index 837a3c58ae..69491cc742 100644 --- a/docs/framework/preact/reference/type-aliases/AppGroupColumnDef.md +++ b/docs/framework/preact/reference/type-aliases/AppGroupColumnDef.md @@ -24,7 +24,7 @@ optional cell: AppColumnDefTemplate[]; +optional columns: ReadonlyArray>; ``` ### footer? diff --git a/docs/framework/preact/reference/type-aliases/FlexRenderProps.md b/docs/framework/preact/reference/type-aliases/FlexRenderProps.md index cfb08d16a6..f3e113d684 100644 --- a/docs/framework/preact/reference/type-aliases/FlexRenderProps.md +++ b/docs/framework/preact/reference/type-aliases/FlexRenderProps.md @@ -6,7 +6,7 @@ title: FlexRenderProps # Type Alias: FlexRenderProps\ ```ts -type FlexRenderProps = +type FlexRenderProps = | { cell: Cell; footer?: never; @@ -24,7 +24,7 @@ type FlexRenderProps = }; ``` -Defined in: [FlexRender.tsx:64](https://github.com/TanStack/table/blob/main/packages/preact-table/src/FlexRender.tsx#L64) +Defined in: [FlexRender.tsx:67](https://github.com/TanStack/table/blob/main/packages/preact-table/src/FlexRender.tsx#L67) Simplified component wrapper of `flexRender`. Use this utility component to render headers, cells, or footers with custom markup. Only one prop (`cell`, `header`, or `footer`) may be passed. diff --git a/docs/framework/preact/reference/type-aliases/PreactTable.md b/docs/framework/preact/reference/type-aliases/PreactTable.md index a3c552c4ca..af6038701b 100644 --- a/docs/framework/preact/reference/type-aliases/PreactTable.md +++ b/docs/framework/preact/reference/type-aliases/PreactTable.md @@ -9,7 +9,7 @@ title: PreactTable type PreactTable = Omit, "store"> & object; ``` -Defined in: [useTable.ts:19](https://github.com/TanStack/table/blob/main/packages/preact-table/src/useTable.ts#L19) +Defined in: [useTable.ts:27](https://github.com/TanStack/table/blob/main/packages/preact-table/src/useTable.ts#L27) ## Type Declaration diff --git a/docs/framework/preact/reference/type-aliases/SubscribeProps.md b/docs/framework/preact/reference/type-aliases/SubscribeProps.md index b12a36c221..5ea4d0e26b 100644 --- a/docs/framework/preact/reference/type-aliases/SubscribeProps.md +++ b/docs/framework/preact/reference/type-aliases/SubscribeProps.md @@ -6,7 +6,7 @@ title: SubscribeProps # Type Alias: SubscribeProps\ ```ts -type SubscribeProps = +type SubscribeProps = | SubscribePropsWithStore | SubscribePropsWithSourceIdentity | SubscribePropsWithSourceWithSelector; diff --git a/docs/framework/preact/reference/type-aliases/SubscribePropsWithSource.md b/docs/framework/preact/reference/type-aliases/SubscribePropsWithSource.md index d6a2e60daf..8d39d4326d 100644 --- a/docs/framework/preact/reference/type-aliases/SubscribePropsWithSource.md +++ b/docs/framework/preact/reference/type-aliases/SubscribePropsWithSource.md @@ -6,7 +6,7 @@ title: SubscribePropsWithSource # Type Alias: SubscribePropsWithSource\ ```ts -type SubscribePropsWithSource = +type SubscribePropsWithSource = | SubscribePropsWithSourceIdentity | SubscribePropsWithSourceWithSelector; ``` diff --git a/docs/framework/preact/reference/type-aliases/SubscribeSource.md b/docs/framework/preact/reference/type-aliases/SubscribeSource.md index 4541094988..1d4fe04102 100644 --- a/docs/framework/preact/reference/type-aliases/SubscribeSource.md +++ b/docs/framework/preact/reference/type-aliases/SubscribeSource.md @@ -6,7 +6,7 @@ title: SubscribeSource # Type Alias: SubscribeSource\ ```ts -type SubscribeSource = +type SubscribeSource = | Atom | ReadonlyAtom | Store diff --git a/docs/framework/react/reference/index/functions/FlexRender-1.md b/docs/framework/react/reference/index/functions/FlexRender-1.md index 1fc66ee6b0..933947b93b 100644 --- a/docs/framework/react/reference/index/functions/FlexRender-1.md +++ b/docs/framework/react/reference/index/functions/FlexRender-1.md @@ -6,7 +6,7 @@ title: FlexRender # Function: FlexRender() ```ts -function FlexRender(props): +function FlexRender(props): | string | number | bigint diff --git a/docs/framework/react/reference/index/functions/useTable.md b/docs/framework/react/reference/index/functions/useTable.md index 7c8a4a1bd6..051c4f6859 100644 --- a/docs/framework/react/reference/index/functions/useTable.md +++ b/docs/framework/react/reference/index/functions/useTable.md @@ -9,7 +9,7 @@ title: useTable function useTable(tableOptions, selector?): ReactTable; ``` -Defined in: [useTable.ts:141](https://github.com/TanStack/table/blob/main/packages/react-table/src/useTable.ts#L141) +Defined in: [useTable.ts:149](https://github.com/TanStack/table/blob/main/packages/react-table/src/useTable.ts#L149) Creates a React table instance backed by TanStack Store atoms. diff --git a/docs/framework/react/reference/index/type-aliases/AppGroupColumnDef.md b/docs/framework/react/reference/index/type-aliases/AppGroupColumnDef.md index f5b71eab3b..dbc86d42a4 100644 --- a/docs/framework/react/reference/index/type-aliases/AppGroupColumnDef.md +++ b/docs/framework/react/reference/index/type-aliases/AppGroupColumnDef.md @@ -24,7 +24,7 @@ optional cell: AppColumnDefTemplate[]; +optional columns: ReadonlyArray>; ``` ### footer? diff --git a/docs/framework/react/reference/index/type-aliases/FlexRenderProps.md b/docs/framework/react/reference/index/type-aliases/FlexRenderProps.md index f4bdb1ca51..c07000c23a 100644 --- a/docs/framework/react/reference/index/type-aliases/FlexRenderProps.md +++ b/docs/framework/react/reference/index/type-aliases/FlexRenderProps.md @@ -6,7 +6,7 @@ title: FlexRenderProps # Type Alias: FlexRenderProps\ ```ts -type FlexRenderProps = +type FlexRenderProps = | { cell: Cell; footer?: never; diff --git a/docs/framework/react/reference/index/type-aliases/ReactTable.md b/docs/framework/react/reference/index/type-aliases/ReactTable.md index ba687dfb15..b8b448527c 100644 --- a/docs/framework/react/reference/index/type-aliases/ReactTable.md +++ b/docs/framework/react/reference/index/type-aliases/ReactTable.md @@ -9,7 +9,7 @@ title: ReactTable type ReactTable = Omit, "store"> & object; ``` -Defined in: [useTable.ts:21](https://github.com/TanStack/table/blob/main/packages/react-table/src/useTable.ts#L21) +Defined in: [useTable.ts:29](https://github.com/TanStack/table/blob/main/packages/react-table/src/useTable.ts#L29) ## Type Declaration diff --git a/docs/framework/react/reference/index/type-aliases/SubscribeProps.md b/docs/framework/react/reference/index/type-aliases/SubscribeProps.md index 41ebae0626..85ae32c40e 100644 --- a/docs/framework/react/reference/index/type-aliases/SubscribeProps.md +++ b/docs/framework/react/reference/index/type-aliases/SubscribeProps.md @@ -6,7 +6,7 @@ title: SubscribeProps # Type Alias: SubscribeProps\ ```ts -type SubscribeProps = +type SubscribeProps = | SubscribePropsWithStore | SubscribePropsWithSourceIdentity | SubscribePropsWithSourceWithSelector; diff --git a/docs/framework/react/reference/index/type-aliases/SubscribePropsWithSource.md b/docs/framework/react/reference/index/type-aliases/SubscribePropsWithSource.md index e08ee8d1d1..4e37fc887b 100644 --- a/docs/framework/react/reference/index/type-aliases/SubscribePropsWithSource.md +++ b/docs/framework/react/reference/index/type-aliases/SubscribePropsWithSource.md @@ -6,7 +6,7 @@ title: SubscribePropsWithSource # Type Alias: SubscribePropsWithSource\ ```ts -type SubscribePropsWithSource = +type SubscribePropsWithSource = | SubscribePropsWithSourceIdentity | SubscribePropsWithSourceWithSelector; ``` diff --git a/docs/framework/react/reference/index/type-aliases/SubscribeSource.md b/docs/framework/react/reference/index/type-aliases/SubscribeSource.md index ed0adc6acc..0ab62bceab 100644 --- a/docs/framework/react/reference/index/type-aliases/SubscribeSource.md +++ b/docs/framework/react/reference/index/type-aliases/SubscribeSource.md @@ -6,7 +6,7 @@ title: SubscribeSource # Type Alias: SubscribeSource\ ```ts -type SubscribeSource = +type SubscribeSource = | Atom | ReadonlyAtom | Store diff --git a/docs/framework/solid/reference/type-aliases/AppGroupColumnDef.md b/docs/framework/solid/reference/type-aliases/AppGroupColumnDef.md index 53f9d1283f..bf362946e1 100644 --- a/docs/framework/solid/reference/type-aliases/AppGroupColumnDef.md +++ b/docs/framework/solid/reference/type-aliases/AppGroupColumnDef.md @@ -24,7 +24,7 @@ optional cell: AppColumnDefTemplate[]; +optional columns: ReadonlyArray>; ``` ### footer? diff --git a/docs/framework/solid/reference/type-aliases/FlexRenderProps.md b/docs/framework/solid/reference/type-aliases/FlexRenderProps.md index ccab421116..baada4bad3 100644 --- a/docs/framework/solid/reference/type-aliases/FlexRenderProps.md +++ b/docs/framework/solid/reference/type-aliases/FlexRenderProps.md @@ -6,7 +6,7 @@ title: FlexRenderProps # Type Alias: FlexRenderProps\ ```ts -type FlexRenderProps = +type FlexRenderProps = | { cell: Cell; footer?: never; diff --git a/docs/framework/svelte/reference/type-aliases/AppGroupColumnDef.md b/docs/framework/svelte/reference/type-aliases/AppGroupColumnDef.md index b02d54e94c..ad1becaec4 100644 --- a/docs/framework/svelte/reference/type-aliases/AppGroupColumnDef.md +++ b/docs/framework/svelte/reference/type-aliases/AppGroupColumnDef.md @@ -24,7 +24,7 @@ optional cell: AppColumnDefTemplate[]; +optional columns: ReadonlyArray>; ``` ### footer? diff --git a/docs/framework/vue/reference/functions/createTableHook.md b/docs/framework/vue/reference/functions/createTableHook.md index f833200ea9..4b02591f82 100644 --- a/docs/framework/vue/reference/functions/createTableHook.md +++ b/docs/framework/vue/reference/functions/createTableHook.md @@ -9,7 +9,7 @@ title: createTableHook function createTableHook(__namedParameters): CreateTableHookResult; ``` -Defined in: [packages/vue-table/src/createTableHook.ts:357](https://github.com/TanStack/table/blob/main/packages/vue-table/src/createTableHook.ts#L357) +Defined in: [packages/vue-table/src/createTableHook.ts:354](https://github.com/TanStack/table/blob/main/packages/vue-table/src/createTableHook.ts#L354) Creates app-scoped Vue table helpers with features, row models, and renderable component maps pre-bound. diff --git a/docs/framework/vue/reference/type-aliases/AppGroupColumnDef.md b/docs/framework/vue/reference/type-aliases/AppGroupColumnDef.md index a96c539039..39edad0917 100644 --- a/docs/framework/vue/reference/type-aliases/AppGroupColumnDef.md +++ b/docs/framework/vue/reference/type-aliases/AppGroupColumnDef.md @@ -22,7 +22,7 @@ optional cell: AppColumnDefTemplate[]; +optional columns: ReadonlyArray>; ``` ### footer? diff --git a/docs/framework/vue/reference/variables/AppFlexRender.md b/docs/framework/vue/reference/variables/AppFlexRender.md index 417ba77549..886bc7f6ef 100644 --- a/docs/framework/vue/reference/variables/AppFlexRender.md +++ b/docs/framework/vue/reference/variables/AppFlexRender.md @@ -19,7 +19,7 @@ const AppFlexRender: DefineComponent; }; -}>, () => +}>, () => | VNode diff --git a/docs/framework/vue/reference/variables/FlexRender.md b/docs/framework/vue/reference/variables/FlexRender.md index dd6ce6f32e..ee2ebc729f 100644 --- a/docs/framework/vue/reference/variables/FlexRender.md +++ b/docs/framework/vue/reference/variables/FlexRender.md @@ -36,7 +36,7 @@ const FlexRender: DefineComponent<{ }, any>; ``` -Defined in: [packages/vue-table/src/FlexRender.ts:67](https://github.com/TanStack/table/blob/main/packages/vue-table/src/FlexRender.ts#L67) +Defined in: [packages/vue-table/src/FlexRender.ts:75](https://github.com/TanStack/table/blob/main/packages/vue-table/src/FlexRender.ts#L75) Simplified component for rendering headers, cells, or footers. diff --git a/docs/guide/table-and-column-meta.md b/docs/guide/table-and-column-meta.md index 3aa0f32cdf..eb1f743e0e 100644 --- a/docs/guide/table-and-column-meta.md +++ b/docs/guide/table-and-column-meta.md @@ -49,6 +49,24 @@ const table = useTable({ table.options.meta?.updateData(rowIndex, columnId, newValue) ``` +# Octane + +```ts +const table = useTable({ + features, + columns, + data, + meta: { + updateData: (rowIndex, columnId, value) => { + // ... + }, + }, +}) + +// ...later, anywhere the table is available (e.g. inside a cell component) +table.options.meta?.updateData(rowIndex, columnId, newValue) +``` + # Vue ```ts @@ -269,6 +287,22 @@ const features = tableFeatures({ }) ``` +# Octane + +```ts +import { + metaHelper, + rowSortingFeature, + tableFeatures, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + rowSortingFeature, + tableMeta: metaHelper(), + columnMeta: metaHelper(), +}) +``` + # Vue ```ts @@ -560,6 +594,26 @@ declare module '@tanstack/preact-table' { } ``` +# Octane + +```ts +import type { CellData, RowData, TableFeatures } from '@tanstack/octane-table' + +declare module '@tanstack/octane-table' { + interface TableMeta { + updateData: (rowIndex: number, columnId: string, value: unknown) => void + } + + interface ColumnMeta< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + > { + filterVariant?: 'text' | 'range' | 'select' + } +} +``` + # Vue ```ts diff --git a/docs/guide/tables.md b/docs/guide/tables.md index fab0fd2906..cf2a6377d1 100644 --- a/docs/guide/tables.md +++ b/docs/guide/tables.md @@ -61,6 +61,20 @@ const table = useTable({ }) ``` +# Octane + +```ts +import { tableFeatures, useTable } from '@tanstack/octane-table' + +const features = tableFeatures({}) // Core features only; add columnFilteringFeature, rowSortingFeature, etc. as needed + +const table = useTable({ + features, + columns, + data, +}) +``` + # Vue ```ts @@ -207,6 +221,12 @@ const table = useTable({ features, columns, data }) const table = useTable({ features, columns, data }) ``` +# Octane + +```ts +const table = useTable({ features, columns, data }) +``` + # Vue ```ts @@ -312,6 +332,10 @@ Direct reads like `table.atoms.rowSelection.get()` and `table.store.state.rowSel [Table State Guide](../framework/preact/guide/table-state) +# Octane + +[Table State Guide](../framework/octane/guide/table-state) + # Vue [Table State Guide](../framework/vue/guide/table-state) diff --git a/docs/guide/worker-row-models.md b/docs/guide/worker-row-models.md index 2d08998b92..b770d93f69 100644 --- a/docs/guide/worker-row-models.md +++ b/docs/guide/worker-row-models.md @@ -94,7 +94,7 @@ const table = useTable({ features, columns, data }) Read `table.state.workerRowModels.isPending` for a loading indicator; `lastComputeMs` and `lastRoundTripMs` on the same slice expose timings. When results arrive, re-renders flow through the standard table state mechanism. -The plugin entry is available from every framework adapter (`@tanstack/react-table/experimental-worker-plugin`, `@tanstack/vue-table/experimental-worker-plugin`, and so on) as well as `@tanstack/table-core/experimental-worker-plugin`, and it is fully tree-shakable: apps that never import it ship none of it. +The plugin entry is available from every framework adapter (`@tanstack/react-table/experimental-worker-plugin`, `@tanstack/octane-table/experimental-worker-plugin`, `@tanstack/vue-table/experimental-worker-plugin`, and so on) as well as `@tanstack/table-core/experimental-worker-plugin`, and it is fully tree-shakable: apps that never import it ship none of it. ## Offloadable Stages and Composition diff --git a/docs/installation.md b/docs/installation.md index f744844847..112d8fe017 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -10,6 +10,7 @@ Install your table adapter as a dependency using your preferred package manager: react: @tanstack/react-table@beta preact: @tanstack/preact-table@beta +octane: @tanstack/octane-table@beta vue: @tanstack/vue-table@beta solid: @tanstack/solid-table@beta svelte: @tanstack/svelte-table@beta @@ -33,6 +34,10 @@ The `@tanstack/react-table` package works with React 18 or newer. The `@tanstack/preact-table` package works with Preact 10 or newer. +# Octane + +The `@tanstack/octane-table` package works with Octane 0.1.21. It publishes authored TSRX, so your application must use Octane's compiler integration (for example, the Octane Vite plugin) to compile the package separately for its client and server targets. + # Vue The `@tanstack/vue-table` package works with Vue 3.2 or newer. diff --git a/docs/overview.md b/docs/overview.md index 6d9e2e12af..9ec0e5e72c 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -14,6 +14,10 @@ title: Overview - [Migrating to v9](./framework/preact/guide/migrating) +# Octane + +- Octane support is new in v9, so there is nothing to migrate. Start with the [Octane Quick Start](./framework/octane/quick-start). + # Solid - [Migrating to v9](./framework/solid/guide/migrating) @@ -54,7 +58,7 @@ In this new age of AI, where markup can be generated in seconds, the old value p TanStack Table might be a bit of a different table library than you are used to. It is **_NOT_** a pre-built table component, like you might find from a library like [AG Grid](https://ag-grid.com?utm_source=reacttable&utm_campaign=githubreacttable). Instead, TanStack Table is a headless UI library that gives you the power to build your own fully customizable table and datagrid components the right way with whatever JavaScript libraries, component libraries, or design systems you want. TanStack Table is the _engine_ that you can hook up to your own favorite front-end tech, no matter what you're already using or want to use. -At its core, TanStack Table is **agnostic**. ~95% of the source code of TanStack Table is written in vanilla, framework-agnostic TypeScript, and thin framework adapters are available for your favorite frameworks. Official adapters are provided for React, Preact, Vue, Solid, Svelte, Angular, Lit, and Alpine, or you can use the core directly in vanilla JavaScript via `@tanstack/table-core`. +At its core, TanStack Table is **agnostic**. ~95% of the source code of TanStack Table is written in vanilla, framework-agnostic TypeScript, and thin framework adapters are available for your favorite frameworks. Official adapters are provided for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine, or you can use the core directly in vanilla JavaScript via `@tanstack/table-core`. It also **does not care which CSS or component library** you use, and is compatible with anything from Tailwind, Bootstrap, Material UI, ShadCN UI, or even your own custom design system. @@ -124,6 +128,31 @@ Here's the difference in practice. With a pre-built table component, you pass pr + ) ``` +# Octane + +```diff +- {/* Pass props into a pre-built component and hope you can style it */} +- ++ // Build your own markup from the table instance's state and APIs ++ const table = useTable({ features, columns, data }) ++ ++ ++ ++ {/* need to really customize the functionality in your headers? no problem! */} ++ @for (const headerGroup of table.getHeaderGroups(); key headerGroup.id) { ... } ++ ++ ++ @for (const row of table.getRowModel().rows; key row.id) { ... } ++ ++
+``` + # Solid ```diff @@ -391,6 +420,23 @@ We like to think of TanStack Table as more of a "system" for building tables tha - [Row Selection](./framework/preact/guide/row-selection) - Select/deselect rows (checkboxes) - [Row Sorting](./framework/preact/guide/sorting) - Sort rows by column values +# Octane + +- [Column Filtering](./framework/octane/guide/column-filtering) - Filter rows based on search values for a column +- [Column Grouping](./framework/octane/guide/grouping) - Group columns together, run aggregations, and more +- [Column Ordering](./framework/octane/guide/column-ordering) - Dynamically change the order of columns +- [Column Pinning](./framework/octane/guide/column-pinning) - Pin (Freeze) columns to the left or right of the table +- [Column Resizing](./framework/octane/guide/column-resizing) - Let users resize columns with drag handles +- [Column Sizing](./framework/octane/guide/column-sizing) - Dynamically change the size of columns +- [Column Visibility](./framework/octane/guide/column-visibility) - Hide/show columns +- [Faceting](./framework/octane/guide/column-faceting) - List unique values or min/max values for a column or for the entire table +- [Global Filtering](./framework/octane/guide/global-filtering) - Filter rows based on search values for the entire table +- [Row Expanding](./framework/octane/guide/expanding) - Expand/collapse rows (sub-rows) +- [Row Pagination](./framework/octane/guide/pagination) - Paginate rows +- [Row Pinning](./framework/octane/guide/row-pinning) - Pin (Freeze) rows to the top or bottom of the table +- [Row Selection](./framework/octane/guide/row-selection) - Select/deselect rows (checkboxes) +- [Row Sorting](./framework/octane/guide/sorting) - Sort rows by column values + # Solid - [Column Filtering](./framework/solid/guide/column-filtering) - Filter rows based on search values for a column @@ -555,6 +601,11 @@ Using a component library? These examples pair TanStack Table with popular React - [Preact Quick Start](./framework/preact/quick-start) - [Kitchen Sink example](./framework/preact/examples/kitchen-sink) - Most of the built-in features working together in one table +# Octane + +- [Octane Quick Start](./framework/octane/quick-start) +- [Kitchen Sink example](./framework/octane/examples/kitchen-sink) - Most of the built-in features working together in one table + # Solid - [Solid Quick Start](./framework/solid/quick-start) diff --git a/docs/reference/index/functions/assignPrototypeAPIs.md b/docs/reference/index/functions/assignPrototypeAPIs.md index 254cb4bf22..14a9a61e99 100644 --- a/docs/reference/index/functions/assignPrototypeAPIs.md +++ b/docs/reference/index/functions/assignPrototypeAPIs.md @@ -7,9 +7,9 @@ title: assignPrototypeAPIs ```ts function assignPrototypeAPIs( - feature, - prototype, - table, + feature, + prototype, + table, apis): void; ``` diff --git a/docs/reference/index/functions/assignTableAPIs.md b/docs/reference/index/functions/assignTableAPIs.md index 9ac2642cd1..312a736270 100644 --- a/docs/reference/index/functions/assignTableAPIs.md +++ b/docs/reference/index/functions/assignTableAPIs.md @@ -7,8 +7,8 @@ title: assignTableAPIs ```ts function assignTableAPIs( - feature, - table, + feature, + table, apis): void; ``` diff --git a/docs/reference/index/functions/buildHeaderGroups.md b/docs/reference/index/functions/buildHeaderGroups.md index b2806b1812..02c63f81fe 100644 --- a/docs/reference/index/functions/buildHeaderGroups.md +++ b/docs/reference/index/functions/buildHeaderGroups.md @@ -7,13 +7,13 @@ title: buildHeaderGroups ```ts function buildHeaderGroups( - allColumns, - columnsToGroup, - table, + allColumns, + columnsToGroup, + table, headerFamily?): HeaderGroup[]; ``` -Defined in: [core/headers/buildHeaderGroups.ts:16](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/headers/buildHeaderGroups.ts#L16) +Defined in: [core/headers/buildHeaderGroups.ts:211](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/headers/buildHeaderGroups.ts#L211) Builds the nested header group structure for a table. diff --git a/docs/reference/index/functions/callMemoOrStaticFn.md b/docs/reference/index/functions/callMemoOrStaticFn.md index d887ea78a8..021992a563 100644 --- a/docs/reference/index/functions/callMemoOrStaticFn.md +++ b/docs/reference/index/functions/callMemoOrStaticFn.md @@ -7,8 +7,8 @@ title: callMemoOrStaticFn ```ts function callMemoOrStaticFn( - obj, - fnKey, + obj, + fnKey, staticFn, ... args): TReturn; ``` diff --git a/docs/reference/index/functions/constructCell.md b/docs/reference/index/functions/constructCell.md index b9d74b310e..507ca4e9fb 100644 --- a/docs/reference/index/functions/constructCell.md +++ b/docs/reference/index/functions/constructCell.md @@ -7,8 +7,8 @@ title: constructCell ```ts function constructCell( - column, - row, + column, + row, table): Cell; ``` diff --git a/docs/reference/index/functions/constructColumn.md b/docs/reference/index/functions/constructColumn.md index 8d5dccb313..20d8c67698 100644 --- a/docs/reference/index/functions/constructColumn.md +++ b/docs/reference/index/functions/constructColumn.md @@ -7,9 +7,9 @@ title: constructColumn ```ts function constructColumn( - table, - columnDef, - depth, + table, + columnDef, + depth, parent?): Column; ``` diff --git a/docs/reference/index/functions/constructHeader.md b/docs/reference/index/functions/constructHeader.md index 095eefbb83..c6be8e235f 100644 --- a/docs/reference/index/functions/constructHeader.md +++ b/docs/reference/index/functions/constructHeader.md @@ -7,8 +7,8 @@ title: constructHeader ```ts function constructHeader( - table, - column, + table, + column, options): Header; ``` diff --git a/docs/reference/index/functions/constructRow.md b/docs/reference/index/functions/constructRow.md index 61d21e514b..61d75025c6 100644 --- a/docs/reference/index/functions/constructRow.md +++ b/docs/reference/index/functions/constructRow.md @@ -7,12 +7,12 @@ title: constructRow ```ts function constructRow( - table, - id, - original, - rowIndex, - depth, - subRows?, + table, + id, + original, + rowIndex, + depth, + subRows?, parentId?): Row; ``` diff --git a/docs/reference/index/functions/constructTable.md b/docs/reference/index/functions/constructTable.md index 553dc74793..8928cc1317 100644 --- a/docs/reference/index/functions/constructTable.md +++ b/docs/reference/index/functions/constructTable.md @@ -9,7 +9,7 @@ title: constructTable function constructTable(tableOptions): Table; ``` -Defined in: [core/table/constructTable.ts:32](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/constructTable.ts#L32) +Defined in: [core/table/constructTable.ts:33](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/constructTable.ts#L33) Constructs a table instance from normalized table internals. diff --git a/docs/reference/index/functions/getFunctionNameInfo.md b/docs/reference/index/functions/getFunctionNameInfo.md index 31cca62ba5..f17cf85fc3 100644 --- a/docs/reference/index/functions/getFunctionNameInfo.md +++ b/docs/reference/index/functions/getFunctionNameInfo.md @@ -21,7 +21,7 @@ Assumes that a function name is in the format of `parentName_fnKey` and returns ### splitBy -`"_"` | `"."` +`"."` | `"_"` ## Returns diff --git a/docs/reference/index/functions/getInitialTableState.md b/docs/reference/index/functions/getInitialTableState.md index 8a55f27c28..d00e9005ef 100644 --- a/docs/reference/index/functions/getInitialTableState.md +++ b/docs/reference/index/functions/getInitialTableState.md @@ -9,7 +9,7 @@ title: getInitialTableState function getInitialTableState(features, initialState): TableState; ``` -Defined in: [core/table/constructTable.ts:17](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/constructTable.ts#L17) +Defined in: [core/table/constructTable.ts:18](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/constructTable.ts#L18) Builds the initial table state from registered features and user initial state. diff --git a/docs/reference/index/functions/makeStateUpdater.md b/docs/reference/index/functions/makeStateUpdater.md index b53ce80ad5..2e5d1ea9b4 100644 --- a/docs/reference/index/functions/makeStateUpdater.md +++ b/docs/reference/index/functions/makeStateUpdater.md @@ -57,7 +57,7 @@ The updater writes through the table base atom for the slice and supports both v #### updater -[`Updater`](../type-aliases/Updater.md)\<`TableState_WorkerRowModels` & [`TableState_RowSorting`](../interfaces/TableState_RowSorting.md) & [`TableState_ColumnPinning`](../interfaces/TableState_ColumnPinning.md) & [`TableState_ColumnSizing`](../interfaces/TableState_ColumnSizing.md) & [`TableState_ColumnResizing`](../interfaces/TableState_ColumnResizing.md) & [`TableState_GlobalFiltering`](../interfaces/TableState_GlobalFiltering.md) & [`TableState_ColumnOrdering`](../interfaces/TableState_ColumnOrdering.md) & [`TableState_ColumnVisibility`](../interfaces/TableState_ColumnVisibility.md) & [`TableState_RowExpanding`](../interfaces/TableState_RowExpanding.md) & [`TableState_RowPinning`](../interfaces/TableState_RowPinning.md) & [`TableState_RowSelection`](../interfaces/TableState_RowSelection.md) & [`TableState_ColumnFiltering`](../interfaces/TableState_ColumnFiltering.md) & [`TableState_RowPagination`](../interfaces/TableState_RowPagination.md) & [`TableState_ColumnGrouping`](../interfaces/TableState_ColumnGrouping.md) & [`TableState_CellSelection`](../interfaces/TableState_CellSelection.md)\[`K` & +[`Updater`](../type-aliases/Updater.md)\<`TableState_WorkerRowModels` & [`TableState_RowSorting`](../interfaces/TableState_RowSorting.md) & [`TableState_ColumnPinning`](../interfaces/TableState_ColumnPinning.md) & [`TableState_ColumnSizing`](../interfaces/TableState_ColumnSizing.md) & [`TableState_ColumnResizing`](../interfaces/TableState_ColumnResizing.md) & [`TableState_GlobalFiltering`](../interfaces/TableState_GlobalFiltering.md) & [`TableState_ColumnOrdering`](../interfaces/TableState_ColumnOrdering.md) & [`TableState_ColumnVisibility`](../interfaces/TableState_ColumnVisibility.md) & [`TableState_RowExpanding`](../interfaces/TableState_RowExpanding.md) & [`TableState_RowPinning`](../interfaces/TableState_RowPinning.md) & [`TableState_RowSelection`](../interfaces/TableState_RowSelection.md) & [`TableState_ColumnFiltering`](../interfaces/TableState_ColumnFiltering.md) & [`TableState_RowPagination`](../interfaces/TableState_RowPagination.md) & [`TableState_ColumnGrouping`](../interfaces/TableState_ColumnGrouping.md) & [`TableState_CellSelection`](../interfaces/TableState_CellSelection.md)\[`K` & \| `"expanded"` \| `"columnFilters"` \| `"globalFilter"` diff --git a/docs/reference/index/interfaces/ColumnDef_RowAggregation.md b/docs/reference/index/interfaces/ColumnDef_RowAggregation.md index 95560b4e4d..5e255aa9df 100644 --- a/docs/reference/index/interfaces/ColumnDef_RowAggregation.md +++ b/docs/reference/index/interfaces/ColumnDef_RowAggregation.md @@ -53,7 +53,7 @@ result object. Inline definitions in an array require an explicit `id`. ### getAggregationValue()? ```ts -optional getAggregationValue: (context) => +optional getAggregationValue: (context) => | AggregationValueResult | undefined; ``` diff --git a/docs/reference/index/interfaces/Column_RowAggregation.md b/docs/reference/index/interfaces/Column_RowAggregation.md index 9dcb5d1643..c559bde39c 100644 --- a/docs/reference/index/interfaces/Column_RowAggregation.md +++ b/docs/reference/index/interfaces/Column_RowAggregation.md @@ -70,7 +70,7 @@ caller-provided array of rows. `options.maxDepth` overrides the column's ### getAutoAggregationFn() ```ts -getAutoAggregationFn: () => +getAutoAggregationFn: () => | AggregationFnDef | undefined; ``` diff --git a/docs/reference/index/interfaces/CreatedFilterFn.md b/docs/reference/index/interfaces/CreatedFilterFn.md index 3855fa32de..eba13cb52c 100644 --- a/docs/reference/index/interfaces/CreatedFilterFn.md +++ b/docs/reference/index/interfaces/CreatedFilterFn.md @@ -30,9 +30,9 @@ any table's rows, regardless of the feature set it was defined against. ```ts CreatedFilterFn( - row, - columnId, - filterValue, + row, + columnId, + filterValue, addMeta?): boolean; ``` diff --git a/docs/reference/index/interfaces/CreatedSortFn.md b/docs/reference/index/interfaces/CreatedSortFn.md index c062d101e1..f665dc0bc4 100644 --- a/docs/reference/index/interfaces/CreatedSortFn.md +++ b/docs/reference/index/interfaces/CreatedSortFn.md @@ -31,8 +31,8 @@ against. ```ts CreatedSortFn( - rowA, - rowB, + rowA, + rowB, columnId): number; ``` diff --git a/docs/reference/index/interfaces/FilterFn.md b/docs/reference/index/interfaces/FilterFn.md index 0b5b1ae732..24d276360e 100644 --- a/docs/reference/index/interfaces/FilterFn.md +++ b/docs/reference/index/interfaces/FilterFn.md @@ -19,9 +19,9 @@ Defined in: [features/column-filtering/columnFilteringFeature.types.ts:60](https ```ts FilterFn( - row, - columnId, - filterValue, + row, + columnId, + filterValue, addMeta?): boolean; ``` diff --git a/docs/reference/index/interfaces/ResolvedAggregationFn.md b/docs/reference/index/interfaces/ResolvedAggregationFn.md index f70e978f5e..e81ee09859 100644 --- a/docs/reference/index/interfaces/ResolvedAggregationFn.md +++ b/docs/reference/index/interfaces/ResolvedAggregationFn.md @@ -24,7 +24,7 @@ A validated aggregation entry returned by `column.getAggregationFns()`. ### aggregationFn ```ts -aggregationFn: +aggregationFn: | AggregationFnDef | undefined; ``` diff --git a/docs/reference/index/interfaces/SortFn.md b/docs/reference/index/interfaces/SortFn.md index b2ce792bc6..455b9ce4eb 100644 --- a/docs/reference/index/interfaces/SortFn.md +++ b/docs/reference/index/interfaces/SortFn.md @@ -19,8 +19,8 @@ Defined in: [features/row-sorting/rowSortingFeature.types.ts:34](https://github. ```ts SortFn( - rowA, - rowB, + rowA, + rowB, columnId): number; ``` diff --git a/docs/reference/index/interfaces/TableFeature.md b/docs/reference/index/interfaces/TableFeature.md index b848353d04..7e98039e17 100644 --- a/docs/reference/index/interfaces/TableFeature.md +++ b/docs/reference/index/interfaces/TableFeature.md @@ -322,7 +322,7 @@ override feature defaults. optional initCellInstanceData: (cell) => void; ``` -Defined in: [types/TableFeatures.ts:449](https://github.com/TanStack/table/blob/main/packages/table-core/src/types/TableFeatures.ts#L449) +Defined in: [types/TableFeatures.ts:452](https://github.com/TanStack/table/blob/main/packages/table-core/src/types/TableFeatures.ts#L452) Initializes instance-specific data on each cell. @@ -364,7 +364,7 @@ Shared methods should be assigned via `assignCellPrototype` instead. optional initColumnInstanceData: (column) => void; ``` -Defined in: [types/TableFeatures.ts:464](https://github.com/TanStack/table/blob/main/packages/table-core/src/types/TableFeatures.ts#L464) +Defined in: [types/TableFeatures.ts:467](https://github.com/TanStack/table/blob/main/packages/table-core/src/types/TableFeatures.ts#L467) Initializes instance-specific data on each column. @@ -405,7 +405,7 @@ methods should be assigned via `assignColumnPrototype` instead. optional initHeaderGroupInstanceData: (headerGroup) => void; ``` -Defined in: [types/TableFeatures.ts:480](https://github.com/TanStack/table/blob/main/packages/table-core/src/types/TableFeatures.ts#L480) +Defined in: [types/TableFeatures.ts:483](https://github.com/TanStack/table/blob/main/packages/table-core/src/types/TableFeatures.ts#L483) Initializes instance-specific data on each header group. @@ -443,7 +443,7 @@ visibility, order, or pinning changes), so this reruns on every rebuild. optional initHeaderInstanceData: (header) => void; ``` -Defined in: [types/TableFeatures.ts:497](https://github.com/TanStack/table/blob/main/packages/table-core/src/types/TableFeatures.ts#L497) +Defined in: [types/TableFeatures.ts:500](https://github.com/TanStack/table/blob/main/packages/table-core/src/types/TableFeatures.ts#L500) Initializes instance-specific data on each header. @@ -487,7 +487,7 @@ instead. optional initRowInstanceData: (row) => void; ``` -Defined in: [types/TableFeatures.ts:512](https://github.com/TanStack/table/blob/main/packages/table-core/src/types/TableFeatures.ts#L512) +Defined in: [types/TableFeatures.ts:515](https://github.com/TanStack/table/blob/main/packages/table-core/src/types/TableFeatures.ts#L515) Initializes instance-specific data on each row. @@ -524,16 +524,19 @@ should be assigned via `assignRowPrototype` instead. optional initTableInstanceData: (table) => void; ``` -Defined in: [types/TableFeatures.ts:434](https://github.com/TanStack/table/blob/main/packages/table-core/src/types/TableFeatures.ts#L434) +Defined in: [types/TableFeatures.ts:437](https://github.com/TanStack/table/blob/main/packages/table-core/src/types/TableFeatures.ts#L437) Initializes mutable, non-reactive data owned by this feature on the table instance. This runs once during table construction after options, state atoms, and -the store are available, and before any feature's `constructTableAPIs` -hook runs. Use `constructTableAPIs` exclusively for assigning table -methods. Table resets do not rerun this hook; use -`resetTableInstanceData` to clear transient instance data instead. +the store are available. Features are processed in a single pass in +registration order, with each feature's instance data initialized just +before its own `constructTableAPIs` hook, so this hook may rely on data +and APIs of features registered earlier. Use `constructTableAPIs` +exclusively for assigning table methods. Table resets do not rerun this +hook; use `resetTableInstanceData` to clear transient instance data +instead. #### Type Parameters @@ -563,7 +566,7 @@ methods. Table resets do not rerun this hook; use optional resetTableInstanceData: (table) => void; ``` -Defined in: [types/TableFeatures.ts:525](https://github.com/TanStack/table/blob/main/packages/table-core/src/types/TableFeatures.ts#L525) +Defined in: [types/TableFeatures.ts:528](https://github.com/TanStack/table/blob/main/packages/table-core/src/types/TableFeatures.ts#L528) Resets mutable, non-reactive table-instance data owned by this feature. diff --git a/docs/reference/index/interfaces/Table_CellSelection.md b/docs/reference/index/interfaces/Table_CellSelection.md index aad0a48f68..55ab7736fe 100644 --- a/docs/reference/index/interfaces/Table_CellSelection.md +++ b/docs/reference/index/interfaces/Table_CellSelection.md @@ -153,7 +153,7 @@ Returns the ids of all rows intersected by the selection. ### getFocusedCell() ```ts -getFocusedCell: () => +getFocusedCell: () => | Cell | undefined; ``` diff --git a/docs/reference/index/interfaces/Table_Columns.md b/docs/reference/index/interfaces/Table_Columns.md index 476a21bb3f..2c245043df 100644 --- a/docs/reference/index/interfaces/Table_Columns.md +++ b/docs/reference/index/interfaces/Table_Columns.md @@ -107,7 +107,7 @@ Returns a map of all leaf-node columns by their ID. This does not include parent ### getColumn() ```ts -getColumn: (columnId) => +getColumn: (columnId) => | Column | undefined; ``` diff --git a/docs/reference/index/interfaces/Table_Core.md b/docs/reference/index/interfaces/Table_Core.md index 744ad71ce9..294c5d5c0a 100644 --- a/docs/reference/index/interfaces/Table_Core.md +++ b/docs/reference/index/interfaces/Table_Core.md @@ -26,10 +26,10 @@ No features are included. ## Properties -### \_cellInstanceInitFns? +### \_cellInstanceInitFns ```ts -optional _cellInstanceInitFns: (cell) => void[]; +_cellInstanceInitFns: (cell) => void[]; ``` Defined in: [core/table/coreTablesFeature.types.ts:167](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L167) @@ -72,7 +72,7 @@ Cache of the `initCellInstanceData` functions for features that define one. optional _cellPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:173](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L173) +Defined in: [core/table/coreTablesFeature.types.ts:171](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L171) Prototype cache for Cell objects - shared by all cells in this table @@ -82,13 +82,13 @@ Prototype cache for Cell objects - shared by all cells in this table *** -### \_columnInstanceInitFns? +### \_columnInstanceInitFns ```ts -optional _columnInstanceInitFns: (column) => void[]; +_columnInstanceInitFns: (column) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:177](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L177) +Defined in: [core/table/coreTablesFeature.types.ts:175](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L175) Cache of the `initColumnInstanceData` functions for features that define one. @@ -128,7 +128,7 @@ Cache of the `initColumnInstanceData` functions for features that define one. optional _columnPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:183](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L183) +Defined in: [core/table/coreTablesFeature.types.ts:181](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L181) Prototype cache for Column objects - shared by all columns in this table @@ -144,7 +144,7 @@ Prototype cache for Column objects - shared by all columns in this table readonly _features: Partial & TFeatures; ``` -Defined in: [core/table/coreTablesFeature.types.ts:187](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L187) +Defined in: [core/table/coreTablesFeature.types.ts:185](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L185) The features that are enabled for the table. @@ -154,13 +154,13 @@ The features that are enabled for the table. *** -### \_headerGroupInstanceInitFns? +### \_headerGroupInstanceInitFns ```ts -optional _headerGroupInstanceInitFns: (headerGroup) => void[]; +_headerGroupInstanceInitFns: (headerGroup) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:191](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L191) +Defined in: [core/table/coreTablesFeature.types.ts:189](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L189) Cache of the `initHeaderGroupInstanceData` functions for features that define one. @@ -190,13 +190,13 @@ Cache of the `initHeaderGroupInstanceData` functions for features that define on *** -### \_headerInstanceInitFns? +### \_headerInstanceInitFns ```ts -optional _headerInstanceInitFns: (header) => void[]; +_headerInstanceInitFns: (header) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:197](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L197) +Defined in: [core/table/coreTablesFeature.types.ts:195](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L195) Cache of the `initHeaderInstanceData` functions for features that define one. @@ -236,7 +236,7 @@ Cache of the `initHeaderInstanceData` functions for features that define one. optional _headerPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:203](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L203) +Defined in: [core/table/coreTablesFeature.types.ts:201](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L201) Prototype cache for Header objects - shared by all headers in this table @@ -262,13 +262,13 @@ Table reactivity bindings for interacting with TanStack Store. *** -### \_rowInstanceInitFns? +### \_rowInstanceInitFns ```ts -optional _rowInstanceInitFns: (row) => void[]; +_rowInstanceInitFns: (row) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:219](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L219) +Defined in: [core/table/coreTablesFeature.types.ts:217](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L217) Cache of the `initRowInstanceData` functions for features that define one. @@ -304,7 +304,7 @@ Cache of the `initRowInstanceData` functions for features that define one. readonly _rowModelFns: RowModelFns; ``` -Defined in: [core/table/coreTablesFeature.types.ts:207](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L207) +Defined in: [core/table/coreTablesFeature.types.ts:205](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L205) The row model processing functions that are used to process the data by features. @@ -320,7 +320,7 @@ The row model processing functions that are used to process the data by features readonly _rowModels: CachedRowModels; ``` -Defined in: [core/table/coreTablesFeature.types.ts:211](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L211) +Defined in: [core/table/coreTablesFeature.types.ts:209](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L209) The row models that are enabled for the table. @@ -336,7 +336,7 @@ The row models that are enabled for the table. optional _rowPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:215](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L215) +Defined in: [core/table/coreTablesFeature.types.ts:213](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L213) Prototype cache for Row objects - shared by all rows in this table @@ -352,7 +352,7 @@ Prototype cache for Row objects - shared by all rows in this table readonly atoms: Atoms; ``` -Defined in: [core/table/coreTablesFeature.types.ts:225](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L225) +Defined in: [core/table/coreTablesFeature.types.ts:223](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L223) The readonly derived atoms for each `TableState` slice. Each derives from its corresponding `baseAtom` plus, optionally, a per-slice external atom or @@ -370,7 +370,7 @@ external state value (precedence: external atom > external state > base atom). readonly baseAtoms: BaseAtoms; ``` -Defined in: [core/table/coreTablesFeature.types.ts:230](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L230) +Defined in: [core/table/coreTablesFeature.types.ts:228](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L228) The internal writable atoms for each `TableState` slice. This is the library's single write surface — all state mutations from features land here. @@ -484,7 +484,7 @@ Returns a map of all leaf-node columns by their ID. This does not include parent ### getColumn() ```ts -getColumn: (columnId) => +getColumn: (columnId) => | Column | undefined; ``` @@ -1062,7 +1062,7 @@ Table_RowModels.getSortedRowModel readonly initialState: ExtractFeatureMapTypes; ``` -Defined in: [core/table/coreTablesFeature.types.ts:234](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L234) +Defined in: [core/table/coreTablesFeature.types.ts:232](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L232) This is the resolved initial state of the table. @@ -1078,7 +1078,7 @@ This is the resolved initial state of the table. readonly options: TableOptions; ``` -Defined in: [core/table/coreTablesFeature.types.ts:238](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L238) +Defined in: [core/table/coreTablesFeature.types.ts:236](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L236) A read-only reference to the table's current options. @@ -1094,7 +1094,7 @@ A read-only reference to the table's current options. readonly optional optionsStore: Atom>; ``` -Defined in: [core/table/coreTablesFeature.types.ts:244](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L244) +Defined in: [core/table/coreTablesFeature.types.ts:242](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L242) Writable atom for table options. Only created when `createOptionsStore` is true on the active core reactivity bindings. Adapters that opt out keep @@ -1112,7 +1112,7 @@ options as plain resolved data instead of backing them with an atom. reset: () => void; ``` -Defined in: [core/table/coreTablesFeature.types.ts:264](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L264) +Defined in: [core/table/coreTablesFeature.types.ts:262](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L262) Resets the table's internal base atoms to `table.initialState`. @@ -1137,7 +1137,7 @@ reset hooks for mutable, transient table-instance data. setOptions: (newOptions) => void; ``` -Defined in: [core/table/coreTablesFeature.types.ts:269](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L269) +Defined in: [core/table/coreTablesFeature.types.ts:267](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L267) Updates the table options by applying a value or updater to the current resolved options and then merging them through `options.mergeOptions`. @@ -1164,7 +1164,7 @@ resolved options and then merging them through `options.mergeOptions`. readonly store: ReadonlyStore>; ``` -Defined in: [core/table/coreTablesFeature.types.ts:249](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L249) +Defined in: [core/table/coreTablesFeature.types.ts:247](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L247) The readonly flat store for the table state. Derives from `table.atoms` only; never reads external state directly. diff --git a/docs/reference/index/interfaces/Table_CoreProperties.md b/docs/reference/index/interfaces/Table_CoreProperties.md index 322a1130b6..6220d12244 100644 --- a/docs/reference/index/interfaces/Table_CoreProperties.md +++ b/docs/reference/index/interfaces/Table_CoreProperties.md @@ -23,10 +23,10 @@ Defined in: [core/table/coreTablesFeature.types.ts:156](https://github.com/TanSt ## Properties -### \_cellInstanceInitFns? +### \_cellInstanceInitFns ```ts -optional _cellInstanceInitFns: (cell) => void[]; +_cellInstanceInitFns: (cell) => void[]; ``` Defined in: [core/table/coreTablesFeature.types.ts:167](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L167) @@ -65,19 +65,19 @@ Cache of the `initCellInstanceData` functions for features that define one. optional _cellPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:173](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L173) +Defined in: [core/table/coreTablesFeature.types.ts:171](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L171) Prototype cache for Cell objects - shared by all cells in this table *** -### \_columnInstanceInitFns? +### \_columnInstanceInitFns ```ts -optional _columnInstanceInitFns: (column) => void[]; +_columnInstanceInitFns: (column) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:177](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L177) +Defined in: [core/table/coreTablesFeature.types.ts:175](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L175) Cache of the `initColumnInstanceData` functions for features that define one. @@ -113,7 +113,7 @@ Cache of the `initColumnInstanceData` functions for features that define one. optional _columnPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:183](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L183) +Defined in: [core/table/coreTablesFeature.types.ts:181](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L181) Prototype cache for Column objects - shared by all columns in this table @@ -125,19 +125,19 @@ Prototype cache for Column objects - shared by all columns in this table readonly _features: Partial & TFeatures; ``` -Defined in: [core/table/coreTablesFeature.types.ts:187](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L187) +Defined in: [core/table/coreTablesFeature.types.ts:185](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L185) The features that are enabled for the table. *** -### \_headerGroupInstanceInitFns? +### \_headerGroupInstanceInitFns ```ts -optional _headerGroupInstanceInitFns: (headerGroup) => void[]; +_headerGroupInstanceInitFns: (headerGroup) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:191](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L191) +Defined in: [core/table/coreTablesFeature.types.ts:189](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L189) Cache of the `initHeaderGroupInstanceData` functions for features that define one. @@ -163,13 +163,13 @@ Cache of the `initHeaderGroupInstanceData` functions for features that define on *** -### \_headerInstanceInitFns? +### \_headerInstanceInitFns ```ts -optional _headerInstanceInitFns: (header) => void[]; +_headerInstanceInitFns: (header) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:197](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L197) +Defined in: [core/table/coreTablesFeature.types.ts:195](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L195) Cache of the `initHeaderInstanceData` functions for features that define one. @@ -205,7 +205,7 @@ Cache of the `initHeaderInstanceData` functions for features that define one. optional _headerPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:203](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L203) +Defined in: [core/table/coreTablesFeature.types.ts:201](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L201) Prototype cache for Header objects - shared by all headers in this table @@ -223,13 +223,13 @@ Table reactivity bindings for interacting with TanStack Store. *** -### \_rowInstanceInitFns? +### \_rowInstanceInitFns ```ts -optional _rowInstanceInitFns: (row) => void[]; +_rowInstanceInitFns: (row) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:219](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L219) +Defined in: [core/table/coreTablesFeature.types.ts:217](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L217) Cache of the `initRowInstanceData` functions for features that define one. @@ -261,7 +261,7 @@ Cache of the `initRowInstanceData` functions for features that define one. readonly _rowModelFns: RowModelFns; ``` -Defined in: [core/table/coreTablesFeature.types.ts:207](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L207) +Defined in: [core/table/coreTablesFeature.types.ts:205](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L205) The row model processing functions that are used to process the data by features. @@ -273,7 +273,7 @@ The row model processing functions that are used to process the data by features readonly _rowModels: CachedRowModels; ``` -Defined in: [core/table/coreTablesFeature.types.ts:211](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L211) +Defined in: [core/table/coreTablesFeature.types.ts:209](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L209) The row models that are enabled for the table. @@ -285,7 +285,7 @@ The row models that are enabled for the table. optional _rowPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:215](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L215) +Defined in: [core/table/coreTablesFeature.types.ts:213](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L213) Prototype cache for Row objects - shared by all rows in this table @@ -297,7 +297,7 @@ Prototype cache for Row objects - shared by all rows in this table readonly atoms: Atoms; ``` -Defined in: [core/table/coreTablesFeature.types.ts:225](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L225) +Defined in: [core/table/coreTablesFeature.types.ts:223](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L223) The readonly derived atoms for each `TableState` slice. Each derives from its corresponding `baseAtom` plus, optionally, a per-slice external atom or @@ -311,7 +311,7 @@ external state value (precedence: external atom > external state > base atom). readonly baseAtoms: BaseAtoms; ``` -Defined in: [core/table/coreTablesFeature.types.ts:230](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L230) +Defined in: [core/table/coreTablesFeature.types.ts:228](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L228) The internal writable atoms for each `TableState` slice. This is the library's single write surface — all state mutations from features land here. @@ -324,7 +324,7 @@ single write surface — all state mutations from features land here. readonly initialState: ExtractFeatureMapTypes; ``` -Defined in: [core/table/coreTablesFeature.types.ts:234](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L234) +Defined in: [core/table/coreTablesFeature.types.ts:232](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L232) This is the resolved initial state of the table. @@ -336,7 +336,7 @@ This is the resolved initial state of the table. readonly options: TableOptions; ``` -Defined in: [core/table/coreTablesFeature.types.ts:238](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L238) +Defined in: [core/table/coreTablesFeature.types.ts:236](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L236) A read-only reference to the table's current options. @@ -348,7 +348,7 @@ A read-only reference to the table's current options. readonly optional optionsStore: Atom>; ``` -Defined in: [core/table/coreTablesFeature.types.ts:244](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L244) +Defined in: [core/table/coreTablesFeature.types.ts:242](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L242) Writable atom for table options. Only created when `createOptionsStore` is true on the active core reactivity bindings. Adapters that opt out keep @@ -362,7 +362,7 @@ options as plain resolved data instead of backing them with an atom. readonly store: ReadonlyStore>; ``` -Defined in: [core/table/coreTablesFeature.types.ts:249](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L249) +Defined in: [core/table/coreTablesFeature.types.ts:247](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L247) The readonly flat store for the table state. Derives from `table.atoms` only; never reads external state directly. diff --git a/docs/reference/index/interfaces/Table_Internal.md b/docs/reference/index/interfaces/Table_Internal.md index 16398441e5..00075352f8 100644 --- a/docs/reference/index/interfaces/Table_Internal.md +++ b/docs/reference/index/interfaces/Table_Internal.md @@ -25,10 +25,10 @@ Internal broad table shape used by feature implementations. ## Properties -### \_cellInstanceInitFns? +### \_cellInstanceInitFns ```ts -optional _cellInstanceInitFns: (cell) => void[]; +_cellInstanceInitFns: (cell) => void[]; ``` Defined in: [core/table/coreTablesFeature.types.ts:167](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L167) @@ -73,7 +73,7 @@ Omit._cellInstanceInitFns optional _cellPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:173](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L173) +Defined in: [core/table/coreTablesFeature.types.ts:171](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L171) Prototype cache for Cell objects - shared by all cells in this table @@ -83,13 +83,13 @@ Prototype cache for Cell objects - shared by all cells in this table *** -### \_columnInstanceInitFns? +### \_columnInstanceInitFns ```ts -optional _columnInstanceInitFns: (column) => void[]; +_columnInstanceInitFns: (column) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:177](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L177) +Defined in: [core/table/coreTablesFeature.types.ts:175](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L175) Cache of the `initColumnInstanceData` functions for features that define one. @@ -131,7 +131,7 @@ Omit._columnInstanceInitFns optional _columnPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:183](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L183) +Defined in: [core/table/coreTablesFeature.types.ts:181](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L181) Prototype cache for Column objects - shared by all columns in this table @@ -147,7 +147,7 @@ Prototype cache for Column objects - shared by all columns in this table readonly _features: Partial & TFeatures; ``` -Defined in: [core/table/coreTablesFeature.types.ts:187](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L187) +Defined in: [core/table/coreTablesFeature.types.ts:185](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L185) The features that are enabled for the table. @@ -159,13 +159,13 @@ Omit._features *** -### \_headerGroupInstanceInitFns? +### \_headerGroupInstanceInitFns ```ts -optional _headerGroupInstanceInitFns: (headerGroup) => void[]; +_headerGroupInstanceInitFns: (headerGroup) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:191](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L191) +Defined in: [core/table/coreTablesFeature.types.ts:189](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L189) Cache of the `initHeaderGroupInstanceData` functions for features that define one. @@ -197,13 +197,13 @@ Omit._headerGroupInstanceInitFns *** -### \_headerInstanceInitFns? +### \_headerInstanceInitFns ```ts -optional _headerInstanceInitFns: (header) => void[]; +_headerInstanceInitFns: (header) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:197](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L197) +Defined in: [core/table/coreTablesFeature.types.ts:195](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L195) Cache of the `initHeaderInstanceData` functions for features that define one. @@ -245,7 +245,7 @@ Omit._headerInstanceInitFns optional _headerPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:203](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L203) +Defined in: [core/table/coreTablesFeature.types.ts:201](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L201) Prototype cache for Header objects - shared by all headers in this table @@ -271,13 +271,13 @@ Table reactivity bindings for interacting with TanStack Store. *** -### \_rowInstanceInitFns? +### \_rowInstanceInitFns ```ts -optional _rowInstanceInitFns: (row) => void[]; +_rowInstanceInitFns: (row) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:219](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L219) +Defined in: [core/table/coreTablesFeature.types.ts:217](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L217) Cache of the `initRowInstanceData` functions for features that define one. @@ -335,7 +335,7 @@ Defined in: [types/Table.ts:106](https://github.com/TanStack/table/blob/main/pac optional _rowPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:215](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L215) +Defined in: [core/table/coreTablesFeature.types.ts:213](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L213) Prototype cache for Row objects - shared by all rows in this table @@ -487,7 +487,7 @@ Returns a map of all leaf-node columns by their ID. This does not include parent ### getColumn() ```ts -getColumn: (columnId) => +getColumn: (columnId) => | Column | undefined; ``` @@ -1164,7 +1164,7 @@ optional state: TableState_All; readonly optional optionsStore: Atom>; ``` -Defined in: [core/table/coreTablesFeature.types.ts:244](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L244) +Defined in: [core/table/coreTablesFeature.types.ts:242](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L242) Writable atom for table options. Only created when `createOptionsStore` is true on the active core reactivity bindings. Adapters that opt out keep @@ -1184,7 +1184,7 @@ Omit.optionsStore reset: () => void; ``` -Defined in: [core/table/coreTablesFeature.types.ts:264](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L264) +Defined in: [core/table/coreTablesFeature.types.ts:262](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L262) Resets the table's internal base atoms to `table.initialState`. @@ -1211,7 +1211,7 @@ Omit.reset setOptions: (newOptions) => void; ``` -Defined in: [core/table/coreTablesFeature.types.ts:269](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L269) +Defined in: [core/table/coreTablesFeature.types.ts:267](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L267) Updates the table options by applying a value or updater to the current resolved options and then merging them through `options.mergeOptions`. diff --git a/docs/reference/index/interfaces/Table_Table.md b/docs/reference/index/interfaces/Table_Table.md index edc13ea2c3..cf9cc3fb63 100644 --- a/docs/reference/index/interfaces/Table_Table.md +++ b/docs/reference/index/interfaces/Table_Table.md @@ -5,7 +5,7 @@ title: Table_Table # Interface: Table\_Table\ -Defined in: [core/table/coreTablesFeature.types.ts:252](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L252) +Defined in: [core/table/coreTablesFeature.types.ts:250](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L250) ## Extends @@ -27,10 +27,10 @@ Defined in: [core/table/coreTablesFeature.types.ts:252](https://github.com/TanSt ## Properties -### \_cellInstanceInitFns? +### \_cellInstanceInitFns ```ts -optional _cellInstanceInitFns: (cell) => void[]; +_cellInstanceInitFns: (cell) => void[]; ``` Defined in: [core/table/coreTablesFeature.types.ts:167](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L167) @@ -73,7 +73,7 @@ Cache of the `initCellInstanceData` functions for features that define one. optional _cellPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:173](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L173) +Defined in: [core/table/coreTablesFeature.types.ts:171](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L171) Prototype cache for Cell objects - shared by all cells in this table @@ -83,13 +83,13 @@ Prototype cache for Cell objects - shared by all cells in this table *** -### \_columnInstanceInitFns? +### \_columnInstanceInitFns ```ts -optional _columnInstanceInitFns: (column) => void[]; +_columnInstanceInitFns: (column) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:177](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L177) +Defined in: [core/table/coreTablesFeature.types.ts:175](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L175) Cache of the `initColumnInstanceData` functions for features that define one. @@ -129,7 +129,7 @@ Cache of the `initColumnInstanceData` functions for features that define one. optional _columnPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:183](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L183) +Defined in: [core/table/coreTablesFeature.types.ts:181](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L181) Prototype cache for Column objects - shared by all columns in this table @@ -145,7 +145,7 @@ Prototype cache for Column objects - shared by all columns in this table readonly _features: Partial & TFeatures; ``` -Defined in: [core/table/coreTablesFeature.types.ts:187](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L187) +Defined in: [core/table/coreTablesFeature.types.ts:185](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L185) The features that are enabled for the table. @@ -155,13 +155,13 @@ The features that are enabled for the table. *** -### \_headerGroupInstanceInitFns? +### \_headerGroupInstanceInitFns ```ts -optional _headerGroupInstanceInitFns: (headerGroup) => void[]; +_headerGroupInstanceInitFns: (headerGroup) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:191](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L191) +Defined in: [core/table/coreTablesFeature.types.ts:189](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L189) Cache of the `initHeaderGroupInstanceData` functions for features that define one. @@ -191,13 +191,13 @@ Cache of the `initHeaderGroupInstanceData` functions for features that define on *** -### \_headerInstanceInitFns? +### \_headerInstanceInitFns ```ts -optional _headerInstanceInitFns: (header) => void[]; +_headerInstanceInitFns: (header) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:197](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L197) +Defined in: [core/table/coreTablesFeature.types.ts:195](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L195) Cache of the `initHeaderInstanceData` functions for features that define one. @@ -237,7 +237,7 @@ Cache of the `initHeaderInstanceData` functions for features that define one. optional _headerPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:203](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L203) +Defined in: [core/table/coreTablesFeature.types.ts:201](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L201) Prototype cache for Header objects - shared by all headers in this table @@ -263,13 +263,13 @@ Table reactivity bindings for interacting with TanStack Store. *** -### \_rowInstanceInitFns? +### \_rowInstanceInitFns ```ts -optional _rowInstanceInitFns: (row) => void[]; +_rowInstanceInitFns: (row) => void[]; ``` -Defined in: [core/table/coreTablesFeature.types.ts:219](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L219) +Defined in: [core/table/coreTablesFeature.types.ts:217](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L217) Cache of the `initRowInstanceData` functions for features that define one. @@ -305,7 +305,7 @@ Cache of the `initRowInstanceData` functions for features that define one. readonly _rowModelFns: RowModelFns; ``` -Defined in: [core/table/coreTablesFeature.types.ts:207](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L207) +Defined in: [core/table/coreTablesFeature.types.ts:205](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L205) The row model processing functions that are used to process the data by features. @@ -321,7 +321,7 @@ The row model processing functions that are used to process the data by features readonly _rowModels: CachedRowModels; ``` -Defined in: [core/table/coreTablesFeature.types.ts:211](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L211) +Defined in: [core/table/coreTablesFeature.types.ts:209](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L209) The row models that are enabled for the table. @@ -337,7 +337,7 @@ The row models that are enabled for the table. optional _rowPrototype: object; ``` -Defined in: [core/table/coreTablesFeature.types.ts:215](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L215) +Defined in: [core/table/coreTablesFeature.types.ts:213](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L213) Prototype cache for Row objects - shared by all rows in this table @@ -353,7 +353,7 @@ Prototype cache for Row objects - shared by all rows in this table readonly atoms: Atoms; ``` -Defined in: [core/table/coreTablesFeature.types.ts:225](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L225) +Defined in: [core/table/coreTablesFeature.types.ts:223](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L223) The readonly derived atoms for each `TableState` slice. Each derives from its corresponding `baseAtom` plus, optionally, a per-slice external atom or @@ -371,7 +371,7 @@ external state value (precedence: external atom > external state > base atom). readonly baseAtoms: BaseAtoms; ``` -Defined in: [core/table/coreTablesFeature.types.ts:230](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L230) +Defined in: [core/table/coreTablesFeature.types.ts:228](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L228) The internal writable atoms for each `TableState` slice. This is the library's single write surface — all state mutations from features land here. @@ -388,7 +388,7 @@ single write surface — all state mutations from features land here. readonly initialState: ExtractFeatureMapTypes; ``` -Defined in: [core/table/coreTablesFeature.types.ts:234](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L234) +Defined in: [core/table/coreTablesFeature.types.ts:232](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L232) This is the resolved initial state of the table. @@ -404,7 +404,7 @@ This is the resolved initial state of the table. readonly options: TableOptions; ``` -Defined in: [core/table/coreTablesFeature.types.ts:238](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L238) +Defined in: [core/table/coreTablesFeature.types.ts:236](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L236) A read-only reference to the table's current options. @@ -420,7 +420,7 @@ A read-only reference to the table's current options. readonly optional optionsStore: Atom>; ``` -Defined in: [core/table/coreTablesFeature.types.ts:244](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L244) +Defined in: [core/table/coreTablesFeature.types.ts:242](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L242) Writable atom for table options. Only created when `createOptionsStore` is true on the active core reactivity bindings. Adapters that opt out keep @@ -438,7 +438,7 @@ options as plain resolved data instead of backing them with an atom. reset: () => void; ``` -Defined in: [core/table/coreTablesFeature.types.ts:264](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L264) +Defined in: [core/table/coreTablesFeature.types.ts:262](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L262) Resets the table's internal base atoms to `table.initialState`. @@ -459,7 +459,7 @@ reset hooks for mutable, transient table-instance data. setOptions: (newOptions) => void; ``` -Defined in: [core/table/coreTablesFeature.types.ts:269](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L269) +Defined in: [core/table/coreTablesFeature.types.ts:267](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L267) Updates the table options by applying a value or updater to the current resolved options and then merging them through `options.mergeOptions`. @@ -482,7 +482,7 @@ resolved options and then merging them through `options.mergeOptions`. readonly store: ReadonlyStore>; ``` -Defined in: [core/table/coreTablesFeature.types.ts:249](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L249) +Defined in: [core/table/coreTablesFeature.types.ts:247](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.types.ts#L247) The readonly flat store for the table state. Derives from `table.atoms` only; never reads external state directly. diff --git a/docs/reference/index/type-aliases/AccessorColumnDef.md b/docs/reference/index/type-aliases/AccessorColumnDef.md index ff7224ad0b..5de3c1754b 100644 --- a/docs/reference/index/type-aliases/AccessorColumnDef.md +++ b/docs/reference/index/type-aliases/AccessorColumnDef.md @@ -6,7 +6,7 @@ title: AccessorColumnDef # Type Alias: AccessorColumnDef\ ```ts -type AccessorColumnDef = +type AccessorColumnDef = | AccessorKeyColumnDef | AccessorFnColumnDef; ``` diff --git a/docs/reference/index/type-aliases/AggregationFnListItem.md b/docs/reference/index/type-aliases/AggregationFnListItem.md index 29c29bb7f7..4b79cbff7b 100644 --- a/docs/reference/index/type-aliases/AggregationFnListItem.md +++ b/docs/reference/index/type-aliases/AggregationFnListItem.md @@ -6,7 +6,7 @@ title: AggregationFnListItem # Type Alias: AggregationFnListItem\ ```ts -type AggregationFnListItem = +type AggregationFnListItem = | "auto" | ExtractAggregationFnKeys | AggregationFnDescriptor; diff --git a/docs/reference/index/type-aliases/AggregationFnOption.md b/docs/reference/index/type-aliases/AggregationFnOption.md index 0488871de1..d889c813c1 100644 --- a/docs/reference/index/type-aliases/AggregationFnOption.md +++ b/docs/reference/index/type-aliases/AggregationFnOption.md @@ -6,7 +6,7 @@ title: AggregationFnOption # Type Alias: AggregationFnOption\ ```ts -type AggregationFnOption = +type AggregationFnOption = | AggregationFnRef | ReadonlyArray>; ``` diff --git a/docs/reference/index/type-aliases/AggregationFnRef.md b/docs/reference/index/type-aliases/AggregationFnRef.md index 45c079bada..fd300a577e 100644 --- a/docs/reference/index/type-aliases/AggregationFnRef.md +++ b/docs/reference/index/type-aliases/AggregationFnRef.md @@ -6,7 +6,7 @@ title: AggregationFnRef # Type Alias: AggregationFnRef\ ```ts -type AggregationFnRef = +type AggregationFnRef = | "auto" | ExtractAggregationFnKeys | AggregationFnDef; diff --git a/docs/reference/index/type-aliases/ColumnAggregationValue.md b/docs/reference/index/type-aliases/ColumnAggregationValue.md index e2ba54e51a..da7d4dd198 100644 --- a/docs/reference/index/type-aliases/ColumnAggregationValue.md +++ b/docs/reference/index/type-aliases/ColumnAggregationValue.md @@ -6,7 +6,7 @@ title: ColumnAggregationValue # Type Alias: ColumnAggregationValue\ ```ts -type ColumnAggregationValue = +type ColumnAggregationValue = | RegisteredAggregationResult | Record | undefined> | undefined; diff --git a/docs/reference/index/type-aliases/ColumnDef.md b/docs/reference/index/type-aliases/ColumnDef.md index c95a1abbed..604692c727 100644 --- a/docs/reference/index/type-aliases/ColumnDef.md +++ b/docs/reference/index/type-aliases/ColumnDef.md @@ -6,7 +6,7 @@ title: ColumnDef # Type Alias: ColumnDef\ ```ts -type ColumnDef = +type ColumnDef = | DisplayColumnDef | GroupColumnDef | AccessorColumnDef; diff --git a/docs/reference/index/type-aliases/ColumnResizingDefaultOptions.md b/docs/reference/index/type-aliases/ColumnResizingDefaultOptions.md index e2087555bd..a440211119 100644 --- a/docs/reference/index/type-aliases/ColumnResizingDefaultOptions.md +++ b/docs/reference/index/type-aliases/ColumnResizingDefaultOptions.md @@ -6,7 +6,7 @@ title: ColumnResizingDefaultOptions # Type Alias: ColumnResizingDefaultOptions ```ts -type ColumnResizingDefaultOptions = Pick; diff --git a/docs/reference/index/type-aliases/DeepKeys.md b/docs/reference/index/type-aliases/DeepKeys.md index 6798bde81f..c63c8b5e32 100644 --- a/docs/reference/index/type-aliases/DeepKeys.md +++ b/docs/reference/index/type-aliases/DeepKeys.md @@ -6,7 +6,7 @@ title: DeepKeys # Type Alias: DeepKeys\ ```ts -type DeepKeys = TDepth["length"] extends 5 ? never : unknown extends T ? string : T extends ReadonlyArray & IsTuple ? +type DeepKeys = TDepth["length"] extends 5 ? never : unknown extends T ? string : T extends ReadonlyArray & IsTuple ? | AllowedIndexes | DeepKeysPrefix, TDepth> : T extends any[] ? DeepKeys : T extends Date ? never : T extends object ? keyof T & string | DeepKeysPrefix : never; ``` diff --git a/docs/reference/index/type-aliases/ExtractAggregationFnKeys.md b/docs/reference/index/type-aliases/ExtractAggregationFnKeys.md index d3f2708036..2f98a1609c 100644 --- a/docs/reference/index/type-aliases/ExtractAggregationFnKeys.md +++ b/docs/reference/index/type-aliases/ExtractAggregationFnKeys.md @@ -6,7 +6,7 @@ title: ExtractAggregationFnKeys # Type Alias: ExtractAggregationFnKeys\ ```ts -type ExtractAggregationFnKeys = IsAny extends true ? +type ExtractAggregationFnKeys = IsAny extends true ? | keyof AggregationFns | BuiltInAggregationFn : TFeatures extends object ? Extract : keyof AggregationFns; ``` diff --git a/docs/reference/index/type-aliases/ExtractFilterFnKeys.md b/docs/reference/index/type-aliases/ExtractFilterFnKeys.md index d52544266d..c0e89e4bb4 100644 --- a/docs/reference/index/type-aliases/ExtractFilterFnKeys.md +++ b/docs/reference/index/type-aliases/ExtractFilterFnKeys.md @@ -6,7 +6,7 @@ title: ExtractFilterFnKeys # Type Alias: ExtractFilterFnKeys\ ```ts -type ExtractFilterFnKeys = IsAny extends true ? +type ExtractFilterFnKeys = IsAny extends true ? | keyof FilterFns | BuiltInFilterFn : TFeatures extends object ? Extract : keyof FilterFns; ``` diff --git a/docs/reference/index/type-aliases/ExtractSortFnKeys.md b/docs/reference/index/type-aliases/ExtractSortFnKeys.md index 3bc5720872..c9ae96f41c 100644 --- a/docs/reference/index/type-aliases/ExtractSortFnKeys.md +++ b/docs/reference/index/type-aliases/ExtractSortFnKeys.md @@ -6,7 +6,7 @@ title: ExtractSortFnKeys # Type Alias: ExtractSortFnKeys\ ```ts -type ExtractSortFnKeys = IsAny extends true ? +type ExtractSortFnKeys = IsAny extends true ? | keyof SortFns | BuiltInSortFn : TFeatures extends object ? Extract : keyof SortFns; ``` diff --git a/docs/reference/index/type-aliases/FilterFnOption.md b/docs/reference/index/type-aliases/FilterFnOption.md index 3499a8b13d..e0b9897252 100644 --- a/docs/reference/index/type-aliases/FilterFnOption.md +++ b/docs/reference/index/type-aliases/FilterFnOption.md @@ -6,7 +6,7 @@ title: FilterFnOption # Type Alias: FilterFnOption\ ```ts -type FilterFnOption = +type FilterFnOption = | "auto" | ExtractFilterFnKeys | FilterFn; diff --git a/docs/reference/index/type-aliases/NonFeatureKeys.md b/docs/reference/index/type-aliases/NonFeatureKeys.md index bb449cf7bb..e28cd46c12 100644 --- a/docs/reference/index/type-aliases/NonFeatureKeys.md +++ b/docs/reference/index/type-aliases/NonFeatureKeys.md @@ -6,7 +6,7 @@ title: NonFeatureKeys # Type Alias: NonFeatureKeys ```ts -type NonFeatureKeys = +type NonFeatureKeys = | "aggregationFns" | "columnMeta" | "coreRowModel" diff --git a/docs/reference/index/type-aliases/SortFnOption.md b/docs/reference/index/type-aliases/SortFnOption.md index bf22fe77c3..61f2e8c62d 100644 --- a/docs/reference/index/type-aliases/SortFnOption.md +++ b/docs/reference/index/type-aliases/SortFnOption.md @@ -6,7 +6,7 @@ title: SortFnOption # Type Alias: SortFnOption\ ```ts -type SortFnOption = +type SortFnOption = | "auto" | ExtractSortFnKeys | SortFn; diff --git a/docs/reference/index/type-aliases/StringOrTemplateHeader.md b/docs/reference/index/type-aliases/StringOrTemplateHeader.md index dbd9378fc7..975dc9edfa 100644 --- a/docs/reference/index/type-aliases/StringOrTemplateHeader.md +++ b/docs/reference/index/type-aliases/StringOrTemplateHeader.md @@ -6,7 +6,7 @@ title: StringOrTemplateHeader # Type Alias: StringOrTemplateHeader\ ```ts -type StringOrTemplateHeader = +type StringOrTemplateHeader = | string | ColumnDefTemplate>; ``` diff --git a/docs/reference/static-functions/functions/aggregateColumnValue.md b/docs/reference/static-functions/functions/aggregateColumnValue.md index 37f7a9d85b..3d483d0c44 100644 --- a/docs/reference/static-functions/functions/aggregateColumnValue.md +++ b/docs/reference/static-functions/functions/aggregateColumnValue.md @@ -9,7 +9,7 @@ title: aggregateColumnValue function aggregateColumnValue(args): unknown; ``` -Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:307](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L307) +Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:337](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L337) Executes every configured aggregation over a depth-selected row frontier. diff --git a/docs/reference/static-functions/functions/cell_getIsAggregated.md b/docs/reference/static-functions/functions/cell_getIsAggregated.md index 11f960b9cb..37c50c34ab 100644 --- a/docs/reference/static-functions/functions/cell_getIsAggregated.md +++ b/docs/reference/static-functions/functions/cell_getIsAggregated.md @@ -9,7 +9,7 @@ title: cell_getIsAggregated function cell_getIsAggregated(cell): boolean; ``` -Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:450](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L450) +Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:480](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L480) Implements `cell.getIsAggregated()` for synthetic grouped rows. diff --git a/docs/reference/static-functions/functions/column_clearSorting.md b/docs/reference/static-functions/functions/column_clearSorting.md index f47ab88d02..8a33162caa 100644 --- a/docs/reference/static-functions/functions/column_clearSorting.md +++ b/docs/reference/static-functions/functions/column_clearSorting.md @@ -9,7 +9,7 @@ title: column_clearSorting function column_clearSorting(column): void; ``` -Defined in: [features/row-sorting/rowSortingFeature.utils.ts:471](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L471) +Defined in: [features/row-sorting/rowSortingFeature.utils.ts:476](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L476) Removes this column from the sorting state. diff --git a/docs/reference/static-functions/functions/column_getAggregationFns.md b/docs/reference/static-functions/functions/column_getAggregationFns.md index 2974ca4a65..33d9eeb6b6 100644 --- a/docs/reference/static-functions/functions/column_getAggregationFns.md +++ b/docs/reference/static-functions/functions/column_getAggregationFns.md @@ -9,7 +9,7 @@ title: column_getAggregationFns function column_getAggregationFns(column): readonly ResolvedAggregationFn[]; ``` -Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:201](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L201) +Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:231](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L231) Resolves and validates a column's scalar or multiple aggregation option. diff --git a/docs/reference/static-functions/functions/column_getAggregationValue.md b/docs/reference/static-functions/functions/column_getAggregationValue.md index 1121ccef2f..1a14f0b3fb 100644 --- a/docs/reference/static-functions/functions/column_getAggregationValue.md +++ b/docs/reference/static-functions/functions/column_getAggregationValue.md @@ -9,7 +9,7 @@ title: column_getAggregationValue function column_getAggregationValue(column, options?): ColumnAggregationValue; ``` -Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:386](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L386) +Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:416](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L416) Implements `column.getAggregationValue(options?)` and its default cache. diff --git a/docs/reference/static-functions/functions/column_getAutoAggregationFn.md b/docs/reference/static-functions/functions/column_getAutoAggregationFn.md index ffd34f4af4..d645dfe581 100644 --- a/docs/reference/static-functions/functions/column_getAutoAggregationFn.md +++ b/docs/reference/static-functions/functions/column_getAutoAggregationFn.md @@ -6,12 +6,12 @@ title: column_getAutoAggregationFn # Function: column\_getAutoAggregationFn() ```ts -function column_getAutoAggregationFn(column): +function column_getAutoAggregationFn(column): | AggregationFnDef | undefined; ``` -Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:159](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L159) +Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:189](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L189) Resolves the `sum` or `extent` definition inferred from the first core row. diff --git a/docs/reference/static-functions/functions/column_getAutoFilterFn.md b/docs/reference/static-functions/functions/column_getAutoFilterFn.md index dbdebaf127..b586997770 100644 --- a/docs/reference/static-functions/functions/column_getAutoFilterFn.md +++ b/docs/reference/static-functions/functions/column_getAutoFilterFn.md @@ -6,7 +6,7 @@ title: column_getAutoFilterFn # Function: column\_getAutoFilterFn() ```ts -function column_getAutoFilterFn(column): +function column_getAutoFilterFn(column): | FilterFn | undefined; ``` diff --git a/docs/reference/static-functions/functions/column_getCanMultiSort.md b/docs/reference/static-functions/functions/column_getCanMultiSort.md index 537236a0b0..423faaa58b 100644 --- a/docs/reference/static-functions/functions/column_getCanMultiSort.md +++ b/docs/reference/static-functions/functions/column_getCanMultiSort.md @@ -9,7 +9,7 @@ title: column_getCanMultiSort function column_getCanMultiSort(column): boolean; ``` -Defined in: [features/row-sorting/rowSortingFeature.utils.ts:406](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L406) +Defined in: [features/row-sorting/rowSortingFeature.utils.ts:411](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L411) Checks whether this column can be added to a multi-sort state. diff --git a/docs/reference/static-functions/functions/column_getCanSort.md b/docs/reference/static-functions/functions/column_getCanSort.md index 6b74553334..7e46369de3 100644 --- a/docs/reference/static-functions/functions/column_getCanSort.md +++ b/docs/reference/static-functions/functions/column_getCanSort.md @@ -9,7 +9,7 @@ title: column_getCanSort function column_getCanSort(column): boolean; ``` -Defined in: [features/row-sorting/rowSortingFeature.utils.ts:383](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L383) +Defined in: [features/row-sorting/rowSortingFeature.utils.ts:388](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L388) Checks whether this accessor column can participate in sorting. diff --git a/docs/reference/static-functions/functions/column_getFilterFn.md b/docs/reference/static-functions/functions/column_getFilterFn.md index 035655ef6b..06c8014cfc 100644 --- a/docs/reference/static-functions/functions/column_getFilterFn.md +++ b/docs/reference/static-functions/functions/column_getFilterFn.md @@ -6,7 +6,7 @@ title: column_getFilterFn # Function: column\_getFilterFn() ```ts -function column_getFilterFn(column): +function column_getFilterFn(column): | FilterFn | undefined; ``` diff --git a/docs/reference/static-functions/functions/column_getFirstSortDir.md b/docs/reference/static-functions/functions/column_getFirstSortDir.md index b00c455475..2a2e59135e 100644 --- a/docs/reference/static-functions/functions/column_getFirstSortDir.md +++ b/docs/reference/static-functions/functions/column_getFirstSortDir.md @@ -9,7 +9,7 @@ title: column_getFirstSortDir function column_getFirstSortDir(column): "asc" | "desc"; ``` -Defined in: [features/row-sorting/rowSortingFeature.utils.ts:327](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L327) +Defined in: [features/row-sorting/rowSortingFeature.utils.ts:332](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L332) Resolves the first direction used when this column begins sorting. diff --git a/docs/reference/static-functions/functions/column_getIsSorted.md b/docs/reference/static-functions/functions/column_getIsSorted.md index 52f98c5829..9d306f268d 100644 --- a/docs/reference/static-functions/functions/column_getIsSorted.md +++ b/docs/reference/static-functions/functions/column_getIsSorted.md @@ -9,7 +9,7 @@ title: column_getIsSorted function column_getIsSorted(column): false | SortDirection; ``` -Defined in: [features/row-sorting/rowSortingFeature.utils.ts:429](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L429) +Defined in: [features/row-sorting/rowSortingFeature.utils.ts:434](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L434) Reads this column's current sort direction. diff --git a/docs/reference/static-functions/functions/column_getNextSortingOrder.md b/docs/reference/static-functions/functions/column_getNextSortingOrder.md index 85be91cfad..51d7c80389 100644 --- a/docs/reference/static-functions/functions/column_getNextSortingOrder.md +++ b/docs/reference/static-functions/functions/column_getNextSortingOrder.md @@ -9,7 +9,7 @@ title: column_getNextSortingOrder function column_getNextSortingOrder(column, multi?): false | "asc" | "desc"; ``` -Defined in: [features/row-sorting/rowSortingFeature.utils.ts:350](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L350) +Defined in: [features/row-sorting/rowSortingFeature.utils.ts:355](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L355) Resolves the next sort order for this column's toggle cycle. diff --git a/docs/reference/static-functions/functions/column_getSortIndex.md b/docs/reference/static-functions/functions/column_getSortIndex.md index 0470db21d7..75b706409b 100644 --- a/docs/reference/static-functions/functions/column_getSortIndex.md +++ b/docs/reference/static-functions/functions/column_getSortIndex.md @@ -9,7 +9,7 @@ title: column_getSortIndex function column_getSortIndex(column): number; ``` -Defined in: [features/row-sorting/rowSortingFeature.utils.ts:450](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L450) +Defined in: [features/row-sorting/rowSortingFeature.utils.ts:455](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L455) Finds this column's position in the ordered `state.sorting` array. diff --git a/docs/reference/static-functions/functions/column_getToggleSortingHandler.md b/docs/reference/static-functions/functions/column_getToggleSortingHandler.md index 5e5cbf5296..8d7b9474c2 100644 --- a/docs/reference/static-functions/functions/column_getToggleSortingHandler.md +++ b/docs/reference/static-functions/functions/column_getToggleSortingHandler.md @@ -9,7 +9,7 @@ title: column_getToggleSortingHandler function column_getToggleSortingHandler(column): (e) => void; ``` -Defined in: [features/row-sorting/rowSortingFeature.utils.ts:493](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L493) +Defined in: [features/row-sorting/rowSortingFeature.utils.ts:498](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-sorting/rowSortingFeature.utils.ts#L498) Creates a header event handler that toggles this column's sorting. diff --git a/docs/reference/static-functions/functions/column_toggleSorting.md b/docs/reference/static-functions/functions/column_toggleSorting.md index 213acbf530..e7a277dae1 100644 --- a/docs/reference/static-functions/functions/column_toggleSorting.md +++ b/docs/reference/static-functions/functions/column_toggleSorting.md @@ -7,8 +7,8 @@ title: column_toggleSorting ```ts function column_toggleSorting( - column, - desc?, + column, + desc?, multi?): void; ``` diff --git a/docs/reference/static-functions/functions/formatAggregatedCellValue.md b/docs/reference/static-functions/functions/formatAggregatedCellValue.md index 2cfa6f6b1d..9f7fc8be77 100644 --- a/docs/reference/static-functions/functions/formatAggregatedCellValue.md +++ b/docs/reference/static-functions/functions/formatAggregatedCellValue.md @@ -9,7 +9,7 @@ title: formatAggregatedCellValue function formatAggregatedCellValue(value, option): string | null; ``` -Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:471](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L471) +Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:501](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L501) Formats the default scalar or keyed aggregated-cell representation. diff --git a/docs/reference/static-functions/functions/header_getContext.md b/docs/reference/static-functions/functions/header_getContext.md index af131167a8..5e301559c8 100644 --- a/docs/reference/static-functions/functions/header_getContext.md +++ b/docs/reference/static-functions/functions/header_getContext.md @@ -9,7 +9,7 @@ title: header_getContext function header_getContext(header): object; ``` -Defined in: [core/headers/coreHeadersFeature.utils.ts:54](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/headers/coreHeadersFeature.utils.ts#L54) +Defined in: [core/headers/coreHeadersFeature.utils.ts:62](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/headers/coreHeadersFeature.utils.ts#L62) Builds the render context passed to a column's `header` or `footer` template. diff --git a/docs/reference/static-functions/functions/header_getLeafHeaders.md b/docs/reference/static-functions/functions/header_getLeafHeaders.md index a7458a9c0e..bfc3efec70 100644 --- a/docs/reference/static-functions/functions/header_getLeafHeaders.md +++ b/docs/reference/static-functions/functions/header_getLeafHeaders.md @@ -9,7 +9,7 @@ title: header_getLeafHeaders function header_getLeafHeaders(header): Header[]; ``` -Defined in: [core/headers/coreHeadersFeature.utils.ts:25](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/headers/coreHeadersFeature.utils.ts#L25) +Defined in: [core/headers/coreHeadersFeature.utils.ts:40](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/headers/coreHeadersFeature.utils.ts#L40) Walks a header tree and collects all descendant leaf headers. diff --git a/docs/reference/static-functions/functions/header_getSize.md b/docs/reference/static-functions/functions/header_getSize.md index fb5d1e0111..2ceb2bc340 100644 --- a/docs/reference/static-functions/functions/header_getSize.md +++ b/docs/reference/static-functions/functions/header_getSize.md @@ -9,7 +9,7 @@ title: header_getSize function header_getSize(header): number; ``` -Defined in: [features/column-sizing/columnSizingFeature.utils.ts:269](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L269) +Defined in: [features/column-sizing/columnSizingFeature.utils.ts:285](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L285) Computes a header's rendered size from its leaf headers. diff --git a/docs/reference/static-functions/functions/header_getStart.md b/docs/reference/static-functions/functions/header_getStart.md index fdaf380548..9b81cd83af 100644 --- a/docs/reference/static-functions/functions/header_getStart.md +++ b/docs/reference/static-functions/functions/header_getStart.md @@ -9,7 +9,7 @@ title: header_getStart function header_getStart(header): number; ``` -Defined in: [features/column-sizing/columnSizingFeature.utils.ts:300](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L300) +Defined in: [features/column-sizing/columnSizingFeature.utils.ts:304](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L304) Computes a header's offset from the start of its header group. diff --git a/docs/reference/static-functions/functions/isRowSelected.md b/docs/reference/static-functions/functions/isRowSelected.md index 1a07476675..55dff63ab1 100644 --- a/docs/reference/static-functions/functions/isRowSelected.md +++ b/docs/reference/static-functions/functions/isRowSelected.md @@ -9,7 +9,7 @@ title: isRowSelected function isRowSelected(row, rowSelection): boolean; ``` -Defined in: [features/row-selection/rowSelectionFeature.utils.ts:866](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts#L866) +Defined in: [features/row-selection/rowSelectionFeature.utils.ts:883](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts#L883) Returns whether a row id is selected in the current row selection state. diff --git a/docs/reference/static-functions/functions/isSubRowSelected.md b/docs/reference/static-functions/functions/isSubRowSelected.md index 2c7e3c5417..bd54c5c215 100644 --- a/docs/reference/static-functions/functions/isSubRowSelected.md +++ b/docs/reference/static-functions/functions/isSubRowSelected.md @@ -9,7 +9,7 @@ title: isSubRowSelected function isSubRowSelected(row): boolean | "some" | "all"; ``` -Defined in: [features/row-selection/rowSelectionFeature.utils.ts:883](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts#L883) +Defined in: [features/row-selection/rowSelectionFeature.utils.ts:900](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts#L900) Returns whether all, some, or none of a row's selectable descendants are selected. diff --git a/docs/reference/static-functions/functions/normalizeAggregationRows.md b/docs/reference/static-functions/functions/normalizeAggregationRows.md index 41d89599a3..8c2805d703 100644 --- a/docs/reference/static-functions/functions/normalizeAggregationRows.md +++ b/docs/reference/static-functions/functions/normalizeAggregationRows.md @@ -9,7 +9,7 @@ title: normalizeAggregationRows function normalizeAggregationRows(rows, maxDepth): Row[]; ``` -Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:66](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L66) +Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:114](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L114) Selects unique rows at a maximum relative depth in encounter order. Branches that end before the requested depth contribute their deepest row. diff --git a/docs/reference/static-functions/functions/normalizeUniqueAggregationRows.md b/docs/reference/static-functions/functions/normalizeUniqueAggregationRows.md index 572a573135..b31e423341 100644 --- a/docs/reference/static-functions/functions/normalizeUniqueAggregationRows.md +++ b/docs/reference/static-functions/functions/normalizeUniqueAggregationRows.md @@ -9,7 +9,7 @@ title: normalizeUniqueAggregationRows function normalizeUniqueAggregationRows(rows, maxDepth): readonly Row[]; ``` -Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:105](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L105) +Defined in: [features/row-aggregation/rowAggregationFeature.utils.ts:145](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-aggregation/rowAggregationFeature.utils.ts#L145) Frontier selection for rows that are distinct nodes of a single row tree — the row models the table builds itself. Skips `normalizeAggregationRows`' diff --git a/docs/reference/static-functions/functions/row_getParentRow.md b/docs/reference/static-functions/functions/row_getParentRow.md index 0d576071cc..bab3987e20 100644 --- a/docs/reference/static-functions/functions/row_getParentRow.md +++ b/docs/reference/static-functions/functions/row_getParentRow.md @@ -6,7 +6,7 @@ title: row_getParentRow # Function: row\_getParentRow() ```ts -function row_getParentRow(row): +function row_getParentRow(row): | Row | undefined; ``` diff --git a/docs/reference/static-functions/functions/row_pin.md b/docs/reference/static-functions/functions/row_pin.md index 023cc05db6..e402fc545a 100644 --- a/docs/reference/static-functions/functions/row_pin.md +++ b/docs/reference/static-functions/functions/row_pin.md @@ -7,9 +7,9 @@ title: row_pin ```ts function row_pin( - row, - position, - includeLeafRows?, + row, + position, + includeLeafRows?, includeParentRows?): void; ``` diff --git a/docs/reference/static-functions/functions/row_toggleSelected.md b/docs/reference/static-functions/functions/row_toggleSelected.md index b971b2c05d..a61dc5291c 100644 --- a/docs/reference/static-functions/functions/row_toggleSelected.md +++ b/docs/reference/static-functions/functions/row_toggleSelected.md @@ -7,8 +7,8 @@ title: row_toggleSelected ```ts function row_toggleSelected( - row, - value?, + row, + value?, opts?): void; ``` diff --git a/docs/reference/static-functions/functions/selectRowsFn.md b/docs/reference/static-functions/functions/selectRowsFn.md index d509066a84..632a072238 100644 --- a/docs/reference/static-functions/functions/selectRowsFn.md +++ b/docs/reference/static-functions/functions/selectRowsFn.md @@ -9,7 +9,7 @@ title: selectRowsFn function selectRowsFn(rowModel, table): RowModel; ``` -Defined in: [features/row-selection/rowSelectionFeature.utils.ts:807](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts#L807) +Defined in: [features/row-selection/rowSelectionFeature.utils.ts:852](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts#L852) Builds a row model containing rows selected by the current row selection state. diff --git a/docs/reference/static-functions/functions/shouldAutoRemoveFilter.md b/docs/reference/static-functions/functions/shouldAutoRemoveFilter.md index 72886e0ec5..e7d5f17e6d 100644 --- a/docs/reference/static-functions/functions/shouldAutoRemoveFilter.md +++ b/docs/reference/static-functions/functions/shouldAutoRemoveFilter.md @@ -7,8 +7,8 @@ title: shouldAutoRemoveFilter ```ts function shouldAutoRemoveFilter( - filterFn?, - value?, + filterFn?, + value?, column?): boolean; ``` diff --git a/docs/reference/static-functions/functions/table_getAllColumns.md b/docs/reference/static-functions/functions/table_getAllColumns.md index 1948b68a8a..c5f73ea4c5 100644 --- a/docs/reference/static-functions/functions/table_getAllColumns.md +++ b/docs/reference/static-functions/functions/table_getAllColumns.md @@ -9,7 +9,7 @@ title: table_getAllColumns function table_getAllColumns(table): Column[]; ``` -Defined in: [core/columns/coreColumnsFeature.utils.ts:120](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts#L120) +Defined in: [core/columns/coreColumnsFeature.utils.ts:155](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts#L155) Normalizes `options.columns` into the table's nested column tree. diff --git a/docs/reference/static-functions/functions/table_getAllFlatColumns.md b/docs/reference/static-functions/functions/table_getAllFlatColumns.md index cba41c9224..97ff3a9a10 100644 --- a/docs/reference/static-functions/functions/table_getAllFlatColumns.md +++ b/docs/reference/static-functions/functions/table_getAllFlatColumns.md @@ -9,7 +9,7 @@ title: table_getAllFlatColumns function table_getAllFlatColumns(table): Column[]; ``` -Defined in: [core/columns/coreColumnsFeature.utils.ts:162](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts#L162) +Defined in: [core/columns/coreColumnsFeature.utils.ts:175](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts#L175) Flattens every table column, including group columns and leaf columns. diff --git a/docs/reference/static-functions/functions/table_getAllFlatColumnsById.md b/docs/reference/static-functions/functions/table_getAllFlatColumnsById.md index da8f8ddebd..6d722fb91b 100644 --- a/docs/reference/static-functions/functions/table_getAllFlatColumnsById.md +++ b/docs/reference/static-functions/functions/table_getAllFlatColumnsById.md @@ -9,7 +9,7 @@ title: table_getAllFlatColumnsById function table_getAllFlatColumnsById(table): Record>; ``` -Defined in: [core/columns/coreColumnsFeature.utils.ts:182](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts#L182) +Defined in: [core/columns/coreColumnsFeature.utils.ts:195](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts#L195) Builds an id lookup for every flat column in the table. diff --git a/docs/reference/static-functions/functions/table_getAllLeafColumns.md b/docs/reference/static-functions/functions/table_getAllLeafColumns.md index c787e828df..ceaaa9e29c 100644 --- a/docs/reference/static-functions/functions/table_getAllLeafColumns.md +++ b/docs/reference/static-functions/functions/table_getAllLeafColumns.md @@ -9,7 +9,7 @@ title: table_getAllLeafColumns function table_getAllLeafColumns(table): Column[]; ``` -Defined in: [core/columns/coreColumnsFeature.utils.ts:208](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts#L208) +Defined in: [core/columns/coreColumnsFeature.utils.ts:221](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts#L221) Collects all terminal leaf columns in their current table order. diff --git a/docs/reference/static-functions/functions/table_getAllLeafColumnsById.md b/docs/reference/static-functions/functions/table_getAllLeafColumnsById.md index 4f394e6701..d2f32eda6f 100644 --- a/docs/reference/static-functions/functions/table_getAllLeafColumnsById.md +++ b/docs/reference/static-functions/functions/table_getAllLeafColumnsById.md @@ -9,7 +9,7 @@ title: table_getAllLeafColumnsById function table_getAllLeafColumnsById(table): Record>; ``` -Defined in: [core/columns/coreColumnsFeature.utils.ts:235](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts#L235) +Defined in: [core/columns/coreColumnsFeature.utils.ts:248](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts#L248) Builds an id lookup for terminal leaf columns only. diff --git a/docs/reference/static-functions/functions/table_getCenterTotalSize.md b/docs/reference/static-functions/functions/table_getCenterTotalSize.md index 844253af9a..22f8379b99 100644 --- a/docs/reference/static-functions/functions/table_getCenterTotalSize.md +++ b/docs/reference/static-functions/functions/table_getCenterTotalSize.md @@ -9,7 +9,7 @@ title: table_getCenterTotalSize function table_getCenterTotalSize(table): number; ``` -Defined in: [features/column-sizing/columnSizingFeature.utils.ts:424](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L424) +Defined in: [features/column-sizing/columnSizingFeature.utils.ts:428](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L428) Sums the rendered size of the center, unpinned header region. diff --git a/docs/reference/static-functions/functions/table_getColumn.md b/docs/reference/static-functions/functions/table_getColumn.md index 84008f7ba7..45f48c1636 100644 --- a/docs/reference/static-functions/functions/table_getColumn.md +++ b/docs/reference/static-functions/functions/table_getColumn.md @@ -6,12 +6,12 @@ title: table_getColumn # Function: table\_getColumn() ```ts -function table_getColumn(table, columnId): +function table_getColumn(table, columnId): | Column | undefined; ``` -Defined in: [core/columns/coreColumnsFeature.utils.ts:261](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts#L261) +Defined in: [core/columns/coreColumnsFeature.utils.ts:274](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts#L274) Looks up a column by id from the flat column map. diff --git a/docs/reference/static-functions/functions/table_getEndTotalSize.md b/docs/reference/static-functions/functions/table_getEndTotalSize.md index b677204375..021a1eefdb 100644 --- a/docs/reference/static-functions/functions/table_getEndTotalSize.md +++ b/docs/reference/static-functions/functions/table_getEndTotalSize.md @@ -9,7 +9,7 @@ title: table_getEndTotalSize function table_getEndTotalSize(table): number; ``` -Defined in: [features/column-sizing/columnSizingFeature.utils.ts:449](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L449) +Defined in: [features/column-sizing/columnSizingFeature.utils.ts:453](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L453) Sums the rendered size of the logical end pinned header region. diff --git a/docs/reference/static-functions/functions/table_getFilteredSelectedRowModel.md b/docs/reference/static-functions/functions/table_getFilteredSelectedRowModel.md index d6b450620d..107e4f63f0 100644 --- a/docs/reference/static-functions/functions/table_getFilteredSelectedRowModel.md +++ b/docs/reference/static-functions/functions/table_getFilteredSelectedRowModel.md @@ -6,7 +6,7 @@ title: table_getFilteredSelectedRowModel # Function: table\_getFilteredSelectedRowModel() ```ts -function table_getFilteredSelectedRowModel(table): +function table_getFilteredSelectedRowModel(table): | RowModel | { flatRows: never[]; diff --git a/docs/reference/static-functions/functions/table_getFlatHeaders.md b/docs/reference/static-functions/functions/table_getFlatHeaders.md index 54d1e90cb7..9921e1bd89 100644 --- a/docs/reference/static-functions/functions/table_getFlatHeaders.md +++ b/docs/reference/static-functions/functions/table_getFlatHeaders.md @@ -9,7 +9,7 @@ title: table_getFlatHeaders function table_getFlatHeaders(table): Header[]; ``` -Defined in: [core/headers/coreHeadersFeature.utils.ts:160](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/headers/coreHeadersFeature.utils.ts#L160) +Defined in: [core/headers/coreHeadersFeature.utils.ts:168](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/headers/coreHeadersFeature.utils.ts#L168) Flattens every header from every header group into one array. diff --git a/docs/reference/static-functions/functions/table_getFocusedCell.md b/docs/reference/static-functions/functions/table_getFocusedCell.md index c6d7711f2c..db647c07c8 100644 --- a/docs/reference/static-functions/functions/table_getFocusedCell.md +++ b/docs/reference/static-functions/functions/table_getFocusedCell.md @@ -6,7 +6,7 @@ title: table_getFocusedCell # Function: table\_getFocusedCell() ```ts -function table_getFocusedCell(table): +function table_getFocusedCell(table): | Cell | undefined; ``` diff --git a/docs/reference/static-functions/functions/table_getFooterGroups.md b/docs/reference/static-functions/functions/table_getFooterGroups.md index dc36974637..5ac22c1c7c 100644 --- a/docs/reference/static-functions/functions/table_getFooterGroups.md +++ b/docs/reference/static-functions/functions/table_getFooterGroups.md @@ -9,7 +9,7 @@ title: table_getFooterGroups function table_getFooterGroups(table): HeaderGroup[]; ``` -Defined in: [core/headers/coreHeadersFeature.utils.ts:141](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/headers/coreHeadersFeature.utils.ts#L141) +Defined in: [core/headers/coreHeadersFeature.utils.ts:149](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/headers/coreHeadersFeature.utils.ts#L149) Builds footer groups by reversing the current header groups. diff --git a/docs/reference/static-functions/functions/table_getGlobalFilterFn.md b/docs/reference/static-functions/functions/table_getGlobalFilterFn.md index ecd5af85b4..289cd748c7 100644 --- a/docs/reference/static-functions/functions/table_getGlobalFilterFn.md +++ b/docs/reference/static-functions/functions/table_getGlobalFilterFn.md @@ -6,7 +6,7 @@ title: table_getGlobalFilterFn # Function: table\_getGlobalFilterFn() ```ts -function table_getGlobalFilterFn(table): +function table_getGlobalFilterFn(table): | FilterFn | undefined; ``` diff --git a/docs/reference/static-functions/functions/table_getGroupedSelectedRowModel.md b/docs/reference/static-functions/functions/table_getGroupedSelectedRowModel.md index 4f92225830..f62a931b5d 100644 --- a/docs/reference/static-functions/functions/table_getGroupedSelectedRowModel.md +++ b/docs/reference/static-functions/functions/table_getGroupedSelectedRowModel.md @@ -6,7 +6,7 @@ title: table_getGroupedSelectedRowModel # Function: table\_getGroupedSelectedRowModel() ```ts -function table_getGroupedSelectedRowModel(table): +function table_getGroupedSelectedRowModel(table): | RowModel | { flatRows: never[]; diff --git a/docs/reference/static-functions/functions/table_getHeaderGroups.md b/docs/reference/static-functions/functions/table_getHeaderGroups.md index 835a03d284..13288adb9b 100644 --- a/docs/reference/static-functions/functions/table_getHeaderGroups.md +++ b/docs/reference/static-functions/functions/table_getHeaderGroups.md @@ -9,7 +9,7 @@ title: table_getHeaderGroups function table_getHeaderGroups(table): HeaderGroup[]; ``` -Defined in: [core/headers/coreHeadersFeature.utils.ts:77](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/headers/coreHeadersFeature.utils.ts#L77) +Defined in: [core/headers/coreHeadersFeature.utils.ts:85](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/headers/coreHeadersFeature.utils.ts#L85) Builds visible header groups for the current column tree. diff --git a/docs/reference/static-functions/functions/table_getLeafHeaders.md b/docs/reference/static-functions/functions/table_getLeafHeaders.md index ae4bcbcb77..3769db8731 100644 --- a/docs/reference/static-functions/functions/table_getLeafHeaders.md +++ b/docs/reference/static-functions/functions/table_getLeafHeaders.md @@ -9,7 +9,7 @@ title: table_getLeafHeaders function table_getLeafHeaders(table): Header[]; ``` -Defined in: [core/headers/coreHeadersFeature.utils.ts:186](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/headers/coreHeadersFeature.utils.ts#L186) +Defined in: [core/headers/coreHeadersFeature.utils.ts:194](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/headers/coreHeadersFeature.utils.ts#L194) Collects only the leaf headers from the current header tree. diff --git a/docs/reference/static-functions/functions/table_getPinnedLeafColumns.md b/docs/reference/static-functions/functions/table_getPinnedLeafColumns.md index d1cb965838..891a177a14 100644 --- a/docs/reference/static-functions/functions/table_getPinnedLeafColumns.md +++ b/docs/reference/static-functions/functions/table_getPinnedLeafColumns.md @@ -6,7 +6,7 @@ title: table_getPinnedLeafColumns # Function: table\_getPinnedLeafColumns() ```ts -function table_getPinnedLeafColumns(table, position): +function table_getPinnedLeafColumns(table, position): | Column[] | Column_Internal[]; ``` diff --git a/docs/reference/static-functions/functions/table_getPinnedVisibleLeafColumns.md b/docs/reference/static-functions/functions/table_getPinnedVisibleLeafColumns.md index 24647f7dd3..7f392def98 100644 --- a/docs/reference/static-functions/functions/table_getPinnedVisibleLeafColumns.md +++ b/docs/reference/static-functions/functions/table_getPinnedVisibleLeafColumns.md @@ -6,7 +6,7 @@ title: table_getPinnedVisibleLeafColumns # Function: table\_getPinnedVisibleLeafColumns() ```ts -function table_getPinnedVisibleLeafColumns(table, position?): +function table_getPinnedVisibleLeafColumns(table, position?): | Column[] | Column_Internal[]; ``` diff --git a/docs/reference/static-functions/functions/table_getRow.md b/docs/reference/static-functions/functions/table_getRow.md index 378ed7708c..18317d4b46 100644 --- a/docs/reference/static-functions/functions/table_getRow.md +++ b/docs/reference/static-functions/functions/table_getRow.md @@ -7,8 +7,8 @@ title: table_getRow ```ts function table_getRow( - table, - rowId, + table, + rowId, searchAll?): Row; ``` diff --git a/docs/reference/static-functions/functions/table_getRowId.md b/docs/reference/static-functions/functions/table_getRowId.md index 495363bdc2..e40068ce1c 100644 --- a/docs/reference/static-functions/functions/table_getRowId.md +++ b/docs/reference/static-functions/functions/table_getRowId.md @@ -7,9 +7,9 @@ title: table_getRowId ```ts function table_getRowId( - originalRow, - table, - index, + originalRow, + table, + index, parent?): string; ``` diff --git a/docs/reference/static-functions/functions/table_getSelectedRowModel.md b/docs/reference/static-functions/functions/table_getSelectedRowModel.md index 6032ad5231..bba142c152 100644 --- a/docs/reference/static-functions/functions/table_getSelectedRowModel.md +++ b/docs/reference/static-functions/functions/table_getSelectedRowModel.md @@ -6,7 +6,7 @@ title: table_getSelectedRowModel # Function: table\_getSelectedRowModel() ```ts -function table_getSelectedRowModel(table): +function table_getSelectedRowModel(table): | RowModel | { flatRows: never[]; diff --git a/docs/reference/static-functions/functions/table_getStartTotalSize.md b/docs/reference/static-functions/functions/table_getStartTotalSize.md index ff23939c93..ade24cf29b 100644 --- a/docs/reference/static-functions/functions/table_getStartTotalSize.md +++ b/docs/reference/static-functions/functions/table_getStartTotalSize.md @@ -9,7 +9,7 @@ title: table_getStartTotalSize function table_getStartTotalSize(table): number; ``` -Defined in: [features/column-sizing/columnSizingFeature.utils.ts:399](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L399) +Defined in: [features/column-sizing/columnSizingFeature.utils.ts:403](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L403) Sums the rendered size of the logical start pinned header region. diff --git a/docs/reference/static-functions/functions/table_getTotalSize.md b/docs/reference/static-functions/functions/table_getTotalSize.md index 27413ddc98..d85f2ee5ee 100644 --- a/docs/reference/static-functions/functions/table_getTotalSize.md +++ b/docs/reference/static-functions/functions/table_getTotalSize.md @@ -9,7 +9,7 @@ title: table_getTotalSize function table_getTotalSize(table): number; ``` -Defined in: [features/column-sizing/columnSizingFeature.utils.ts:378](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L378) +Defined in: [features/column-sizing/columnSizingFeature.utils.ts:382](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L382) Sums the rendered size of the full table header row. diff --git a/docs/reference/static-functions/functions/table_mergeOptions.md b/docs/reference/static-functions/functions/table_mergeOptions.md index d5bec237d7..72249ee53f 100644 --- a/docs/reference/static-functions/functions/table_mergeOptions.md +++ b/docs/reference/static-functions/functions/table_mergeOptions.md @@ -9,7 +9,7 @@ title: table_mergeOptions function table_mergeOptions(table, newOptions): TableOptions; ``` -Defined in: [core/table/coreTablesFeature.utils.ts:86](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.utils.ts#L86) +Defined in: [core/table/coreTablesFeature.utils.ts:128](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.utils.ts#L128) Merges new table options with the current resolved options. diff --git a/docs/reference/static-functions/functions/table_publishExternalState.md b/docs/reference/static-functions/functions/table_publishExternalState.md new file mode 100644 index 0000000000..6bc5dc2afa --- /dev/null +++ b/docs/reference/static-functions/functions/table_publishExternalState.md @@ -0,0 +1,49 @@ +--- +id: table_publishExternalState +title: table_publishExternalState +--- + +# Function: table\_publishExternalState() + +```ts +function table_publishExternalState( + table, + state, + compare): void; +``` + +Defined in: [core/table/coreTablesFeature.utils.ts:67](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.utils.ts#L67) + +Publishes captured controlled state after a host framework commits. + +Render-phase adapters stage options without synchronizing base atoms, then +pass the state captured by the committed render here. The commit signal also +invalidates ownership changes when no base atom was written. + +## Type Parameters + +### TFeatures + +`TFeatures` *extends* [`TableFeatures`](../../index/interfaces/TableFeatures.md) + +### TData + +`TData` *extends* [`RowData`](../../index/type-aliases/RowData.md) + +## Parameters + +### table + +[`Table_Internal`](../../index/interfaces/Table_Internal.md)\<`TFeatures`, `TData`\> + +### state + +`Partial`\<[`TableState`](../../index/type-aliases/TableState.md)\<`TFeatures`\>\> | `null` + +### compare + +(`currentState`, `externalState`) => `boolean` + +## Returns + +`void` diff --git a/docs/reference/static-functions/functions/table_reset.md b/docs/reference/static-functions/functions/table_reset.md index ec07464dc7..d18096907a 100644 --- a/docs/reference/static-functions/functions/table_reset.md +++ b/docs/reference/static-functions/functions/table_reset.md @@ -9,7 +9,7 @@ title: table_reset function table_reset(table): void; ``` -Defined in: [core/table/coreTablesFeature.utils.ts:54](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.utils.ts#L54) +Defined in: [core/table/coreTablesFeature.utils.ts:96](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.utils.ts#L96) Resets all internal table base atoms to `table.initialState`, then clears transient instance data through registered feature reset hooks. diff --git a/docs/reference/static-functions/functions/table_resetColumnSizing.md b/docs/reference/static-functions/functions/table_resetColumnSizing.md index 0ac96277e3..af979c7427 100644 --- a/docs/reference/static-functions/functions/table_resetColumnSizing.md +++ b/docs/reference/static-functions/functions/table_resetColumnSizing.md @@ -9,7 +9,7 @@ title: table_resetColumnSizing function table_resetColumnSizing(table, defaultState?): void; ``` -Defined in: [features/column-sizing/columnSizingFeature.utils.ts:353](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L353) +Defined in: [features/column-sizing/columnSizingFeature.utils.ts:357](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L357) Resets `columnSizing` to the configured initial state or feature default. diff --git a/docs/reference/static-functions/functions/table_selectCellRange.md b/docs/reference/static-functions/functions/table_selectCellRange.md index 5a70937fdc..07b634aa80 100644 --- a/docs/reference/static-functions/functions/table_selectCellRange.md +++ b/docs/reference/static-functions/functions/table_selectCellRange.md @@ -7,8 +7,8 @@ title: table_selectCellRange ```ts function table_selectCellRange( - table, - range, + table, + range, opts?): void; ``` diff --git a/docs/reference/static-functions/functions/table_setColumnSizing.md b/docs/reference/static-functions/functions/table_setColumnSizing.md index 921e2596d4..5f024f2bb0 100644 --- a/docs/reference/static-functions/functions/table_setColumnSizing.md +++ b/docs/reference/static-functions/functions/table_setColumnSizing.md @@ -9,7 +9,7 @@ title: table_setColumnSizing function table_setColumnSizing(table, updater): void; ``` -Defined in: [features/column-sizing/columnSizingFeature.utils.ts:331](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L331) +Defined in: [features/column-sizing/columnSizingFeature.utils.ts:335](https://github.com/TanStack/table/blob/main/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts#L335) Routes a committed column sizing updater through the table's sizing handler. diff --git a/docs/reference/static-functions/functions/table_setFocusedCell.md b/docs/reference/static-functions/functions/table_setFocusedCell.md index 8b3de469b1..8928cb101a 100644 --- a/docs/reference/static-functions/functions/table_setFocusedCell.md +++ b/docs/reference/static-functions/functions/table_setFocusedCell.md @@ -7,8 +7,8 @@ title: table_setFocusedCell ```ts function table_setFocusedCell( - table, - rowId, + table, + rowId, columnId): void; ``` diff --git a/docs/reference/static-functions/functions/table_setOptions.md b/docs/reference/static-functions/functions/table_setOptions.md index 4ca8ba7278..762ad41258 100644 --- a/docs/reference/static-functions/functions/table_setOptions.md +++ b/docs/reference/static-functions/functions/table_setOptions.md @@ -6,10 +6,13 @@ title: table_setOptions # Function: table\_setOptions() ```ts -function table_setOptions(table, updater): void; +function table_setOptions( + table, + updater, + options?): void; ``` -Defined in: [core/table/coreTablesFeature.utils.ts:152](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.utils.ts#L152) +Defined in: [core/table/coreTablesFeature.utils.ts:195](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.utils.ts#L195) Updates the table options object. @@ -36,6 +39,12 @@ immediately assigned to the table instance. [`Updater`](../../index/type-aliases/Updater.md)\<[`TableOptions`](../../index/type-aliases/TableOptions.md)\<`TFeatures`, `TData`\>\> +### options? + +#### syncExternalState? + +`boolean` + ## Returns `void` @@ -44,4 +53,5 @@ immediately assigned to the table instance. ```ts table_setOptions(table, (old) => old) +table_setOptions(table, (old) => old, { syncExternalState: false }) ``` diff --git a/docs/reference/static-functions/functions/table_syncExternalStateToBaseAtoms.md b/docs/reference/static-functions/functions/table_syncExternalStateToBaseAtoms.md index c3a8961ebd..231579c262 100644 --- a/docs/reference/static-functions/functions/table_syncExternalStateToBaseAtoms.md +++ b/docs/reference/static-functions/functions/table_syncExternalStateToBaseAtoms.md @@ -6,15 +6,26 @@ title: table_syncExternalStateToBaseAtoms # Function: table\_syncExternalStateToBaseAtoms() ```ts -function table_syncExternalStateToBaseAtoms(table): void; +function table_syncExternalStateToBaseAtoms( + table, + capturedState?, + compare?): void; ``` -Defined in: [core/table/coreTablesFeature.utils.ts:18](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.utils.ts#L18) +Defined in: [core/table/coreTablesFeature.utils.ts:28](https://github.com/TanStack/table/blob/main/packages/table-core/src/core/table/coreTablesFeature.utils.ts#L28) Synchronizes externally controlled state slices into the table's base atoms. -This keeps legacy `options.state` values reflected in the atom graph so -derived atoms, stores, and table APIs read a consistent snapshot. +This keeps `options.state` values mirrored in the atom graph so derived +atoms, stores, and table APIs read a consistent snapshot. + +Adapters that update options during their host's render phase pass the +state snapshot captured by the committed render as `capturedState` — the +shared options object may already hold values from a newer render that +never commits. Pass `null` to publish nothing (a captured "no controlled +state"); omitting the argument reads the current `table.options.state` +instead. An optional `compare` suppresses semantically unchanged slice +writes; the default remains reference equality. ## Type Parameters @@ -32,6 +43,14 @@ derived atoms, stores, and table APIs read a consistent snapshot. [`Table_Internal`](../../index/interfaces/Table_Internal.md)\<`TFeatures`, `TData`\> +### capturedState? + +`Partial`\<[`TableState`](../../index/type-aliases/TableState.md)\<`TFeatures`\>\> | `null` + +### compare? + +(`currentState`, `externalState`) => `boolean` + ## Returns `void` @@ -40,4 +59,5 @@ derived atoms, stores, and table APIs read a consistent snapshot. ```ts table_syncExternalStateToBaseAtoms(table) +table_syncExternalStateToBaseAtoms(table, capturedState ?? null, shallow) ``` diff --git a/docs/reference/static-functions/functions/table_toggleAllPageRowsSelected.md b/docs/reference/static-functions/functions/table_toggleAllPageRowsSelected.md index d746770b17..aefbd0cacf 100644 --- a/docs/reference/static-functions/functions/table_toggleAllPageRowsSelected.md +++ b/docs/reference/static-functions/functions/table_toggleAllPageRowsSelected.md @@ -7,8 +7,8 @@ title: table_toggleAllPageRowsSelected ```ts function table_toggleAllPageRowsSelected( - table, - value?, + table, + value?, opts?): void; ``` diff --git a/docs/reference/static-functions/functions/table_toggleAllRowsSelected.md b/docs/reference/static-functions/functions/table_toggleAllRowsSelected.md index 975c733b69..aace83efa1 100644 --- a/docs/reference/static-functions/functions/table_toggleAllRowsSelected.md +++ b/docs/reference/static-functions/functions/table_toggleAllRowsSelected.md @@ -7,8 +7,8 @@ title: table_toggleAllRowsSelected ```ts function table_toggleAllRowsSelected( - table, - value?, + table, + value?, opts?): void; ``` diff --git a/docs/reference/static-functions/index.md b/docs/reference/static-functions/index.md index b5ca28f5cb..930ee30025 100644 --- a/docs/reference/static-functions/index.md +++ b/docs/reference/static-functions/index.md @@ -241,6 +241,7 @@ title: static-functions - [table\_moveCellSelection](functions/table_moveCellSelection.md) - [table\_nextPage](functions/table_nextPage.md) - [table\_previousPage](functions/table_previousPage.md) +- [table\_publishExternalState](functions/table_publishExternalState.md) - [table\_reset](functions/table_reset.md) - [table\_resetCellSelection](functions/table_resetCellSelection.md) - [table\_resetColumnFilters](functions/table_resetColumnFilters.md) diff --git a/examples/alpine/aggregation/vite.config.js b/examples/alpine/aggregation/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/aggregation/vite.config.js +++ b/examples/alpine/aggregation/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/basic-app-table/vite.config.js b/examples/alpine/basic-app-table/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/basic-app-table/vite.config.js +++ b/examples/alpine/basic-app-table/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/basic-create-table/vite.config.js b/examples/alpine/basic-create-table/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/basic-create-table/vite.config.js +++ b/examples/alpine/basic-create-table/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/basic-dynamic-columns/vite.config.js b/examples/alpine/basic-dynamic-columns/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/basic-dynamic-columns/vite.config.js +++ b/examples/alpine/basic-dynamic-columns/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/basic-external-atoms/vite.config.js b/examples/alpine/basic-external-atoms/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/basic-external-atoms/vite.config.js +++ b/examples/alpine/basic-external-atoms/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/basic-external-state/vite.config.js b/examples/alpine/basic-external-state/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/basic-external-state/vite.config.js +++ b/examples/alpine/basic-external-state/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/cell-selection/vite.config.js b/examples/alpine/cell-selection/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/cell-selection/vite.config.js +++ b/examples/alpine/cell-selection/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/column-ordering/vite.config.js b/examples/alpine/column-ordering/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/column-ordering/vite.config.js +++ b/examples/alpine/column-ordering/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/column-pinning-split/vite.config.js b/examples/alpine/column-pinning-split/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/column-pinning-split/vite.config.js +++ b/examples/alpine/column-pinning-split/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/column-pinning-sticky/vite.config.js b/examples/alpine/column-pinning-sticky/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/column-pinning-sticky/vite.config.js +++ b/examples/alpine/column-pinning-sticky/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/column-pinning/vite.config.js b/examples/alpine/column-pinning/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/column-pinning/vite.config.js +++ b/examples/alpine/column-pinning/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/column-resizing-performant/vite.config.js b/examples/alpine/column-resizing-performant/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/column-resizing-performant/vite.config.js +++ b/examples/alpine/column-resizing-performant/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/column-resizing/vite.config.js b/examples/alpine/column-resizing/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/column-resizing/vite.config.js +++ b/examples/alpine/column-resizing/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/column-sizing/vite.config.js b/examples/alpine/column-sizing/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/column-sizing/vite.config.js +++ b/examples/alpine/column-sizing/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/column-visibility/vite.config.js b/examples/alpine/column-visibility/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/column-visibility/vite.config.js +++ b/examples/alpine/column-visibility/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/custom-plugin/vite.config.js b/examples/alpine/custom-plugin/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/custom-plugin/vite.config.js +++ b/examples/alpine/custom-plugin/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/expanding/vite.config.js b/examples/alpine/expanding/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/expanding/vite.config.js +++ b/examples/alpine/expanding/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/filters-faceted-bucketed/vite.config.js b/examples/alpine/filters-faceted-bucketed/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/filters-faceted-bucketed/vite.config.js +++ b/examples/alpine/filters-faceted-bucketed/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/filters-faceted/vite.config.js b/examples/alpine/filters-faceted/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/filters-faceted/vite.config.js +++ b/examples/alpine/filters-faceted/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/filters/vite.config.js b/examples/alpine/filters/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/filters/vite.config.js +++ b/examples/alpine/filters/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/grouped-aggregation/vite.config.js b/examples/alpine/grouped-aggregation/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/grouped-aggregation/vite.config.js +++ b/examples/alpine/grouped-aggregation/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/grouping/vite.config.js b/examples/alpine/grouping/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/grouping/vite.config.js +++ b/examples/alpine/grouping/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/header-groups/vite.config.js b/examples/alpine/header-groups/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/header-groups/vite.config.js +++ b/examples/alpine/header-groups/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/pagination/vite.config.js b/examples/alpine/pagination/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/pagination/vite.config.js +++ b/examples/alpine/pagination/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/row-pinning/vite.config.js b/examples/alpine/row-pinning/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/row-pinning/vite.config.js +++ b/examples/alpine/row-pinning/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/row-selection/vite.config.js b/examples/alpine/row-selection/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/row-selection/vite.config.js +++ b/examples/alpine/row-selection/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/sorting-dynamic-data/vite.config.js b/examples/alpine/sorting-dynamic-data/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/sorting-dynamic-data/vite.config.js +++ b/examples/alpine/sorting-dynamic-data/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/sorting/vite.config.js b/examples/alpine/sorting/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/sorting/vite.config.js +++ b/examples/alpine/sorting/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/alpine/sub-components/vite.config.js b/examples/alpine/sub-components/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/alpine/sub-components/vite.config.js +++ b/examples/alpine/sub-components/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/angular/aggregation/package.json b/examples/angular/aggregation/package.json index 3d00ac389c..d949268f0f 100644 --- a/examples/angular/aggregation/package.json +++ b/examples/angular/aggregation/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-aggregation", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/basic-app-table/package.json b/examples/angular/basic-app-table/package.json index 13055e5d9b..561ccf8409 100644 --- a/examples/angular/basic-app-table/package.json +++ b/examples/angular/basic-app-table/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-basic-app-table", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "lint": "eslint ./src", "test:types": "tsc -p tsconfig.app.json --noEmit", diff --git a/examples/angular/basic-dynamic-columns/package.json b/examples/angular/basic-dynamic-columns/package.json index 4fe94df52e..0fb1c5478e 100644 --- a/examples/angular/basic-dynamic-columns/package.json +++ b/examples/angular/basic-dynamic-columns/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-basic-dynamic-columns", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "lint": "eslint ./src", "test:types": "tsc -p tsconfig.app.json --noEmit", diff --git a/examples/angular/basic-external-atoms/package.json b/examples/angular/basic-external-atoms/package.json index 9658342d7d..3dc746de85 100644 --- a/examples/angular/basic-external-atoms/package.json +++ b/examples/angular/basic-external-atoms/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-basic-external-atoms", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "lint": "eslint ./src", "test:types": "tsc -p tsconfig.app.json --noEmit", diff --git a/examples/angular/basic-external-state/package.json b/examples/angular/basic-external-state/package.json index 7b8c91eba2..c779e4b9be 100644 --- a/examples/angular/basic-external-state/package.json +++ b/examples/angular/basic-external-state/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-basic-external-state", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "lint": "eslint ./src", "test:types": "tsc -p tsconfig.app.json --noEmit", diff --git a/examples/angular/basic-inject-table/package.json b/examples/angular/basic-inject-table/package.json index 328539c577..2ca9f34700 100644 --- a/examples/angular/basic-inject-table/package.json +++ b/examples/angular/basic-inject-table/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-basic-inject-table", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/cell-selection/package.json b/examples/angular/cell-selection/package.json index 51cc93771f..f07570c454 100644 --- a/examples/angular/cell-selection/package.json +++ b/examples/angular/cell-selection/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-cell-selection", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/column-ordering/package.json b/examples/angular/column-ordering/package.json index 223fb7c64f..ff44d5e6e8 100644 --- a/examples/angular/column-ordering/package.json +++ b/examples/angular/column-ordering/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-column-ordering", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "lint": "eslint ./src", "test:types": "tsc -p tsconfig.app.json --noEmit", diff --git a/examples/angular/column-pinning-split/package.json b/examples/angular/column-pinning-split/package.json index ab3c8ffd4b..88c449b8a2 100644 --- a/examples/angular/column-pinning-split/package.json +++ b/examples/angular/column-pinning-split/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-column-pinning-split", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/column-pinning-sticky/package.json b/examples/angular/column-pinning-sticky/package.json index ffb5f6622d..67d9ae3580 100644 --- a/examples/angular/column-pinning-sticky/package.json +++ b/examples/angular/column-pinning-sticky/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-column-pinning-sticky", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "lint": "eslint ./src", "test:types": "tsc -p tsconfig.app.json --noEmit", diff --git a/examples/angular/column-pinning/package.json b/examples/angular/column-pinning/package.json index d1f8628459..47af2b4ceb 100644 --- a/examples/angular/column-pinning/package.json +++ b/examples/angular/column-pinning/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-column-pinning", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "lint": "eslint ./src", "test:types": "tsc -p tsconfig.app.json --noEmit", diff --git a/examples/angular/column-resizing-performant/package.json b/examples/angular/column-resizing-performant/package.json index dcc91023ee..cfb194760c 100644 --- a/examples/angular/column-resizing-performant/package.json +++ b/examples/angular/column-resizing-performant/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-column-resizing-performant", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "lint": "eslint ./src", "test:types": "tsc -p tsconfig.app.json --noEmit", diff --git a/examples/angular/column-resizing/package.json b/examples/angular/column-resizing/package.json index 6f21dc8d17..144c53810e 100644 --- a/examples/angular/column-resizing/package.json +++ b/examples/angular/column-resizing/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-column-resizing", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/column-sizing/package.json b/examples/angular/column-sizing/package.json index 8ac4e18d43..d314fe1e2b 100644 --- a/examples/angular/column-sizing/package.json +++ b/examples/angular/column-sizing/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-column-sizing", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/column-visibility/package.json b/examples/angular/column-visibility/package.json index 4474e480f0..182d91924e 100644 --- a/examples/angular/column-visibility/package.json +++ b/examples/angular/column-visibility/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-column-visibility", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "lint": "eslint ./src", "test:types": "tsc -p tsconfig.app.json --noEmit", diff --git a/examples/angular/composable-tables/package.json b/examples/angular/composable-tables/package.json index 98717de184..4a6478e938 100644 --- a/examples/angular/composable-tables/package.json +++ b/examples/angular/composable-tables/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-composable-tables", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "test:types": "tsc -p tsconfig.app.json --noEmit", "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", diff --git a/examples/angular/custom-plugin/package.json b/examples/angular/custom-plugin/package.json index b029a523c2..970f46e84a 100644 --- a/examples/angular/custom-plugin/package.json +++ b/examples/angular/custom-plugin/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-custom-plugin", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/editable/package.json b/examples/angular/editable/package.json index 1b05c90c3b..0a4ebb8e1a 100644 --- a/examples/angular/editable/package.json +++ b/examples/angular/editable/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-editable", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "lint": "eslint ./src", "test:types": "tsc -p tsconfig.app.json --noEmit", diff --git a/examples/angular/expanding/package.json b/examples/angular/expanding/package.json index 87fbdb390b..5179e4c8cd 100644 --- a/examples/angular/expanding/package.json +++ b/examples/angular/expanding/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-expanding", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "lint": "eslint ./src", "test:types": "tsc -p tsconfig.app.json --noEmit", diff --git a/examples/angular/filters-faceted-bucketed/package.json b/examples/angular/filters-faceted-bucketed/package.json index b919c03527..2b0e5b6870 100644 --- a/examples/angular/filters-faceted-bucketed/package.json +++ b/examples/angular/filters-faceted-bucketed/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-filters-faceted-bucketed", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "lint": "eslint ./src", "test:types": "tsc -p tsconfig.app.json --noEmit", diff --git a/examples/angular/filters-faceted/package.json b/examples/angular/filters-faceted/package.json index aa3030d2d1..9e7dfb43ac 100644 --- a/examples/angular/filters-faceted/package.json +++ b/examples/angular/filters-faceted/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-filters-faceted", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "lint": "eslint ./src", "test:types": "tsc -p tsconfig.app.json --noEmit", diff --git a/examples/angular/filters-fuzzy/package.json b/examples/angular/filters-fuzzy/package.json index 951515b00a..77d8cd6000 100644 --- a/examples/angular/filters-fuzzy/package.json +++ b/examples/angular/filters-fuzzy/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-filters-fuzzy", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/filters/package.json b/examples/angular/filters/package.json index df99cf5ff1..112fcd7468 100644 --- a/examples/angular/filters/package.json +++ b/examples/angular/filters/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-filters", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "lint": "eslint ./src", "test:types": "tsc -p tsconfig.app.json --noEmit", diff --git a/examples/angular/grouped-aggregation/package.json b/examples/angular/grouped-aggregation/package.json index 2c3ea94693..87e40b67ae 100644 --- a/examples/angular/grouped-aggregation/package.json +++ b/examples/angular/grouped-aggregation/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-grouped-aggregation", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/grouping/package.json b/examples/angular/grouping/package.json index 97325408bb..7010cd28c7 100644 --- a/examples/angular/grouping/package.json +++ b/examples/angular/grouping/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-grouping", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/header-groups/package.json b/examples/angular/header-groups/package.json index 74dc4a343d..902a9d4118 100644 --- a/examples/angular/header-groups/package.json +++ b/examples/angular/header-groups/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-header-groups", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/kitchen-sink/package.json b/examples/angular/kitchen-sink/package.json index ada84b343a..6a4cbc7339 100644 --- a/examples/angular/kitchen-sink/package.json +++ b/examples/angular/kitchen-sink/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-kitchen-sink", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "lint": "eslint ./src", "test:types": "tsc -p tsconfig.app.json --noEmit", diff --git a/examples/angular/pagination/package.json b/examples/angular/pagination/package.json index cca84d661b..f71f6d4a8a 100644 --- a/examples/angular/pagination/package.json +++ b/examples/angular/pagination/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-pagination", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/remote-data/package.json b/examples/angular/remote-data/package.json index 58a98bb894..87c36f2f68 100644 --- a/examples/angular/remote-data/package.json +++ b/examples/angular/remote-data/package.json @@ -2,9 +2,9 @@ "name": "tanstack-angular-table-example-remote-data", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", - "start:no-ssr": "ng serve --port 6565 --configuration=no-ssr", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", + "start:no-ssr": "ng serve --port 7777 --configuration=no-ssr", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", diff --git a/examples/angular/row-dnd/package.json b/examples/angular/row-dnd/package.json index 3ebff7052e..7650730272 100644 --- a/examples/angular/row-dnd/package.json +++ b/examples/angular/row-dnd/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-row-dnd", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/row-pinning/package.json b/examples/angular/row-pinning/package.json index 5f44d7f4ed..42964d2868 100644 --- a/examples/angular/row-pinning/package.json +++ b/examples/angular/row-pinning/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-row-pinning", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/row-selection-signal/package.json b/examples/angular/row-selection-signal/package.json index 31e6aa92a3..bd2f55f6cc 100644 --- a/examples/angular/row-selection-signal/package.json +++ b/examples/angular/row-selection-signal/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-row-selection-signal", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", diff --git a/examples/angular/row-selection/package.json b/examples/angular/row-selection/package.json index 1be6ec1d2b..476dd0745e 100644 --- a/examples/angular/row-selection/package.json +++ b/examples/angular/row-selection/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-row-selection", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/signal-input/package.json b/examples/angular/signal-input/package.json index da9c0ae7a1..c1d6647d3c 100644 --- a/examples/angular/signal-input/package.json +++ b/examples/angular/signal-input/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-signal-input", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/sorting/package.json b/examples/angular/sorting/package.json index c3b024e106..7e3d862713 100644 --- a/examples/angular/sorting/package.json +++ b/examples/angular/sorting/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-sorting", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/sub-components/package.json b/examples/angular/sub-components/package.json index 533a2d6d61..39f9e522cf 100644 --- a/examples/angular/sub-components/package.json +++ b/examples/angular/sub-components/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-sub-components", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/virtualized-columns/package.json b/examples/angular/virtualized-columns/package.json index 6c689d912e..0676e8d5db 100644 --- a/examples/angular/virtualized-columns/package.json +++ b/examples/angular/virtualized-columns/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-virtualized-columns", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/virtualized-infinite-scrolling/package.json b/examples/angular/virtualized-infinite-scrolling/package.json index aec60476e6..8347e31dc4 100644 --- a/examples/angular/virtualized-infinite-scrolling/package.json +++ b/examples/angular/virtualized-infinite-scrolling/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-virtualized-infinite-scrolling", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/virtualized-rows/package.json b/examples/angular/virtualized-rows/package.json index bd08629bb2..eac068b127 100644 --- a/examples/angular/virtualized-rows/package.json +++ b/examples/angular/virtualized-rows/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-virtualized-rows", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/with-tanstack-form/package.json b/examples/angular/with-tanstack-form/package.json index a967f28461..2c15817307 100644 --- a/examples/angular/with-tanstack-form/package.json +++ b/examples/angular/with-tanstack-form/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-with-tanstack-form", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/angular/with-tanstack-query/package.json b/examples/angular/with-tanstack-query/package.json index 8eae25a802..9b6bd8fc49 100644 --- a/examples/angular/with-tanstack-query/package.json +++ b/examples/angular/with-tanstack-query/package.json @@ -2,8 +2,8 @@ "name": "tanstack-angular-table-example-with-tanstack-query", "scripts": { "ng": "ng", - "start": "ng serve --port 6565", - "dev": "ng serve --port 6565", + "start": "ng serve --port 7777", + "dev": "ng serve --port 7777", "build": "ng build", "watch": "ng build --watch --configuration development", "lint": "eslint ./src", diff --git a/examples/ember/aggregation/vite.config.mjs b/examples/ember/aggregation/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/aggregation/vite.config.mjs +++ b/examples/ember/aggregation/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/basic-app-table/vite.config.mjs b/examples/ember/basic-app-table/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/basic-app-table/vite.config.mjs +++ b/examples/ember/basic-app-table/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/basic-external-atoms/vite.config.mjs b/examples/ember/basic-external-atoms/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/basic-external-atoms/vite.config.mjs +++ b/examples/ember/basic-external-atoms/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/basic-external-state/vite.config.mjs b/examples/ember/basic-external-state/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/basic-external-state/vite.config.mjs +++ b/examples/ember/basic-external-state/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/basic-table/vite.config.mjs b/examples/ember/basic-table/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/basic-table/vite.config.mjs +++ b/examples/ember/basic-table/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/cell-selection/vite.config.mjs b/examples/ember/cell-selection/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/cell-selection/vite.config.mjs +++ b/examples/ember/cell-selection/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/column-ordering/vite.config.mjs b/examples/ember/column-ordering/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/column-ordering/vite.config.mjs +++ b/examples/ember/column-ordering/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/column-pinning-split/vite.config.mjs b/examples/ember/column-pinning-split/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/column-pinning-split/vite.config.mjs +++ b/examples/ember/column-pinning-split/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/column-pinning-sticky/vite.config.mjs b/examples/ember/column-pinning-sticky/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/column-pinning-sticky/vite.config.mjs +++ b/examples/ember/column-pinning-sticky/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/column-pinning/vite.config.mjs b/examples/ember/column-pinning/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/column-pinning/vite.config.mjs +++ b/examples/ember/column-pinning/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/column-resizing-performant/vite.config.mjs b/examples/ember/column-resizing-performant/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/column-resizing-performant/vite.config.mjs +++ b/examples/ember/column-resizing-performant/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/column-resizing/vite.config.mjs b/examples/ember/column-resizing/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/column-resizing/vite.config.mjs +++ b/examples/ember/column-resizing/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/column-sizing/vite.config.mjs b/examples/ember/column-sizing/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/column-sizing/vite.config.mjs +++ b/examples/ember/column-sizing/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/column-visibility/vite.config.mjs b/examples/ember/column-visibility/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/column-visibility/vite.config.mjs +++ b/examples/ember/column-visibility/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/custom-plugin/vite.config.mjs b/examples/ember/custom-plugin/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/custom-plugin/vite.config.mjs +++ b/examples/ember/custom-plugin/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/editable/vite.config.mjs b/examples/ember/editable/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/editable/vite.config.mjs +++ b/examples/ember/editable/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/expanding/vite.config.mjs b/examples/ember/expanding/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/expanding/vite.config.mjs +++ b/examples/ember/expanding/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/filters-faceted-bucketed/vite.config.mjs b/examples/ember/filters-faceted-bucketed/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/filters-faceted-bucketed/vite.config.mjs +++ b/examples/ember/filters-faceted-bucketed/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/filters-faceted/vite.config.mjs b/examples/ember/filters-faceted/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/filters-faceted/vite.config.mjs +++ b/examples/ember/filters-faceted/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/filters-fuzzy/vite.config.mjs b/examples/ember/filters-fuzzy/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/filters-fuzzy/vite.config.mjs +++ b/examples/ember/filters-fuzzy/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/filters/vite.config.mjs b/examples/ember/filters/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/filters/vite.config.mjs +++ b/examples/ember/filters/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/grouped-aggregation/vite.config.mjs b/examples/ember/grouped-aggregation/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/grouped-aggregation/vite.config.mjs +++ b/examples/ember/grouped-aggregation/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/grouping/vite.config.mjs b/examples/ember/grouping/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/grouping/vite.config.mjs +++ b/examples/ember/grouping/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/header-groups/vite.config.mjs b/examples/ember/header-groups/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/header-groups/vite.config.mjs +++ b/examples/ember/header-groups/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/kitchen-sink/vite.config.mjs b/examples/ember/kitchen-sink/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/kitchen-sink/vite.config.mjs +++ b/examples/ember/kitchen-sink/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/pagination/vite.config.mjs b/examples/ember/pagination/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/pagination/vite.config.mjs +++ b/examples/ember/pagination/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/remote-data/vite.config.mjs b/examples/ember/remote-data/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/remote-data/vite.config.mjs +++ b/examples/ember/remote-data/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/row-dnd/vite.config.mjs b/examples/ember/row-dnd/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/row-dnd/vite.config.mjs +++ b/examples/ember/row-dnd/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/row-pinning/vite.config.mjs b/examples/ember/row-pinning/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/row-pinning/vite.config.mjs +++ b/examples/ember/row-pinning/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/row-selection/vite.config.mjs b/examples/ember/row-selection/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/row-selection/vite.config.mjs +++ b/examples/ember/row-selection/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/sorting/vite.config.mjs b/examples/ember/sorting/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/sorting/vite.config.mjs +++ b/examples/ember/sorting/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/ember/sub-components/vite.config.mjs b/examples/ember/sub-components/vite.config.mjs index 25d29c7c5e..cd42e2e9d1 100644 --- a/examples/ember/sub-components/vite.config.mjs +++ b/examples/ember/sub-components/vite.config.mjs @@ -3,7 +3,7 @@ import { ember } from '@nullvoxpopuli/ember-vite' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ember()], }) diff --git a/examples/lit/aggregation/vite.config.js b/examples/lit/aggregation/vite.config.js index 16eb98bb27..06909749e4 100644 --- a/examples/lit/aggregation/vite.config.js +++ b/examples/lit/aggregation/vite.config.js @@ -3,7 +3,7 @@ import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/basic-app-table/vite.config.js b/examples/lit/basic-app-table/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/basic-app-table/vite.config.js +++ b/examples/lit/basic-app-table/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/basic-dynamic-columns/vite.config.js b/examples/lit/basic-dynamic-columns/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/basic-dynamic-columns/vite.config.js +++ b/examples/lit/basic-dynamic-columns/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/basic-external-atoms/vite.config.js b/examples/lit/basic-external-atoms/vite.config.js index 16eb98bb27..06909749e4 100644 --- a/examples/lit/basic-external-atoms/vite.config.js +++ b/examples/lit/basic-external-atoms/vite.config.js @@ -3,7 +3,7 @@ import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/basic-external-state/vite.config.js b/examples/lit/basic-external-state/vite.config.js index 16eb98bb27..06909749e4 100644 --- a/examples/lit/basic-external-state/vite.config.js +++ b/examples/lit/basic-external-state/vite.config.js @@ -3,7 +3,7 @@ import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/basic-subscribe/vite.config.js b/examples/lit/basic-subscribe/vite.config.js index 16eb98bb27..06909749e4 100644 --- a/examples/lit/basic-subscribe/vite.config.js +++ b/examples/lit/basic-subscribe/vite.config.js @@ -3,7 +3,7 @@ import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/basic-table-controller/vite.config.js b/examples/lit/basic-table-controller/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/basic-table-controller/vite.config.js +++ b/examples/lit/basic-table-controller/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/cell-selection/vite.config.js b/examples/lit/cell-selection/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/cell-selection/vite.config.js +++ b/examples/lit/cell-selection/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/column-ordering/vite.config.js b/examples/lit/column-ordering/vite.config.js index 79107d7f17..e622f70fa2 100644 --- a/examples/lit/column-ordering/vite.config.js +++ b/examples/lit/column-ordering/vite.config.js @@ -2,7 +2,7 @@ import { defineConfig } from 'vite' import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/column-pinning-split/vite.config.js b/examples/lit/column-pinning-split/vite.config.js index 79107d7f17..e622f70fa2 100644 --- a/examples/lit/column-pinning-split/vite.config.js +++ b/examples/lit/column-pinning-split/vite.config.js @@ -2,7 +2,7 @@ import { defineConfig } from 'vite' import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/column-pinning-sticky/vite.config.js b/examples/lit/column-pinning-sticky/vite.config.js index 79107d7f17..e622f70fa2 100644 --- a/examples/lit/column-pinning-sticky/vite.config.js +++ b/examples/lit/column-pinning-sticky/vite.config.js @@ -2,7 +2,7 @@ import { defineConfig } from 'vite' import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/column-pinning/vite.config.js b/examples/lit/column-pinning/vite.config.js index 79107d7f17..e622f70fa2 100644 --- a/examples/lit/column-pinning/vite.config.js +++ b/examples/lit/column-pinning/vite.config.js @@ -2,7 +2,7 @@ import { defineConfig } from 'vite' import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/column-resizing-performant/vite.config.js b/examples/lit/column-resizing-performant/vite.config.js index 79107d7f17..e622f70fa2 100644 --- a/examples/lit/column-resizing-performant/vite.config.js +++ b/examples/lit/column-resizing-performant/vite.config.js @@ -2,7 +2,7 @@ import { defineConfig } from 'vite' import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/column-resizing/vite.config.js b/examples/lit/column-resizing/vite.config.js index 79107d7f17..e622f70fa2 100644 --- a/examples/lit/column-resizing/vite.config.js +++ b/examples/lit/column-resizing/vite.config.js @@ -2,7 +2,7 @@ import { defineConfig } from 'vite' import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/column-visibility/vite.config.js b/examples/lit/column-visibility/vite.config.js index 16eb98bb27..06909749e4 100644 --- a/examples/lit/column-visibility/vite.config.js +++ b/examples/lit/column-visibility/vite.config.js @@ -3,7 +3,7 @@ import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/composable-tables/vite.config.js b/examples/lit/composable-tables/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/composable-tables/vite.config.js +++ b/examples/lit/composable-tables/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/expanding/vite.config.js b/examples/lit/expanding/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/expanding/vite.config.js +++ b/examples/lit/expanding/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/filters-faceted-bucketed/vite.config.js b/examples/lit/filters-faceted-bucketed/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/filters-faceted-bucketed/vite.config.js +++ b/examples/lit/filters-faceted-bucketed/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/filters-faceted/vite.config.js b/examples/lit/filters-faceted/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/filters-faceted/vite.config.js +++ b/examples/lit/filters-faceted/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/filters-fuzzy/vite.config.js b/examples/lit/filters-fuzzy/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/filters-fuzzy/vite.config.js +++ b/examples/lit/filters-fuzzy/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/filters/vite.config.js b/examples/lit/filters/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/filters/vite.config.js +++ b/examples/lit/filters/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/grouped-aggregation/vite.config.js b/examples/lit/grouped-aggregation/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/grouped-aggregation/vite.config.js +++ b/examples/lit/grouped-aggregation/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/grouping/vite.config.js b/examples/lit/grouping/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/grouping/vite.config.js +++ b/examples/lit/grouping/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/header-groups/vite.config.js b/examples/lit/header-groups/vite.config.js index 79107d7f17..e622f70fa2 100644 --- a/examples/lit/header-groups/vite.config.js +++ b/examples/lit/header-groups/vite.config.js @@ -2,7 +2,7 @@ import { defineConfig } from 'vite' import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/kitchen-sink/src/main.ts b/examples/lit/kitchen-sink/src/main.ts index 254d74384b..581fb7dc4d 100644 --- a/examples/lit/kitchen-sink/src/main.ts +++ b/examples/lit/kitchen-sink/src/main.ts @@ -206,7 +206,7 @@ class LitTableExample extends LitElement { keepPinnedRows: true, debugTable: true, }, - (state) => state, + (state) => state, // default selector ) return html` diff --git a/examples/lit/kitchen-sink/vite.config.js b/examples/lit/kitchen-sink/vite.config.js index 79107d7f17..e622f70fa2 100644 --- a/examples/lit/kitchen-sink/vite.config.js +++ b/examples/lit/kitchen-sink/vite.config.js @@ -2,7 +2,7 @@ import { defineConfig } from 'vite' import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/pagination/vite.config.js b/examples/lit/pagination/vite.config.js index 16eb98bb27..06909749e4 100644 --- a/examples/lit/pagination/vite.config.js +++ b/examples/lit/pagination/vite.config.js @@ -3,7 +3,7 @@ import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/row-pinning/vite.config.js b/examples/lit/row-pinning/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/row-pinning/vite.config.js +++ b/examples/lit/row-pinning/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/row-selection/vite.config.js b/examples/lit/row-selection/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/row-selection/vite.config.js +++ b/examples/lit/row-selection/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/sorting-dynamic-data/vite.config.js b/examples/lit/sorting-dynamic-data/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/sorting-dynamic-data/vite.config.js +++ b/examples/lit/sorting-dynamic-data/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/sorting/vite.config.js b/examples/lit/sorting/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/sorting/vite.config.js +++ b/examples/lit/sorting/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/sub-components/vite.config.js b/examples/lit/sub-components/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/sub-components/vite.config.js +++ b/examples/lit/sub-components/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/virtualized-columns/vite.config.js b/examples/lit/virtualized-columns/vite.config.js index 79107d7f17..e622f70fa2 100644 --- a/examples/lit/virtualized-columns/vite.config.js +++ b/examples/lit/virtualized-columns/vite.config.js @@ -2,7 +2,7 @@ import { defineConfig } from 'vite' import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/virtualized-infinite-scrolling/vite.config.js b/examples/lit/virtualized-infinite-scrolling/vite.config.js index 79107d7f17..e622f70fa2 100644 --- a/examples/lit/virtualized-infinite-scrolling/vite.config.js +++ b/examples/lit/virtualized-infinite-scrolling/vite.config.js @@ -2,7 +2,7 @@ import { defineConfig } from 'vite' import rollupReplace from '@rollup/plugin-replace' export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/lit/virtualized-rows/vite.config.js b/examples/lit/virtualized-rows/vite.config.js index 9b5122fda5..7a74450ca4 100644 --- a/examples/lit/virtualized-rows/vite.config.js +++ b/examples/lit/virtualized-rows/vite.config.js @@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 6565, + port: 7777, }, plugins: [ rollupReplace({ diff --git a/examples/octane/aggregation/index.html b/examples/octane/aggregation/index.html new file mode 100644 index 0000000000..347491962b --- /dev/null +++ b/examples/octane/aggregation/index.html @@ -0,0 +1,13 @@ + + + + + + + TanStack Preact Table - Aggregation + + +
+ + + diff --git a/examples/octane/aggregation/package.json b/examples/octane/aggregation/package.json new file mode 100644 index 0000000000..03f600c7a8 --- /dev/null +++ b/examples/octane/aggregation/package.json @@ -0,0 +1,24 @@ +{ + "name": "@tanstack/octane-table-example-aggregation", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-store": "^0.12.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/aggregation/src/index.css b/examples/octane/aggregation/src/index.css new file mode 100644 index 0000000000..6aa3364a5e --- /dev/null +++ b/examples/octane/aggregation/src/index.css @@ -0,0 +1,87 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; + text-align: left; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; +} + +.demo-root { + padding: 0.5rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.controls, +.inline-controls { + display: flex; + align-items: center; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls { + gap: 0.25rem; +} + +.demo-button, +.page-size-input { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +td { + padding: 2px 4px; +} + +.numeric { + text-align: right; +} + +code { + background: #f0f0f0; + padding: 0.1rem 0.25rem; +} diff --git a/examples/octane/aggregation/src/main.tsrx b/examples/octane/aggregation/src/main.tsrx new file mode 100644 index 0000000000..965baad9f6 --- /dev/null +++ b/examples/octane/aggregation/src/main.tsrx @@ -0,0 +1,309 @@ +import { createRoot } from 'octane' +import { useMemo, useState } from 'octane' +import './index.css' +import { + rowAggregationFeature, + aggregationFn_count, + aggregationFn_extent, + aggregationFn_mean, + aggregationFn_sum, + columnFilteringFeature, + createColumnHelper, + createFilteredRowModel, + createPaginatedRowModel, + filterFn_includesString, + metaHelper, + rowPaginationFeature, + rowSelectionFeature, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Table } from '@tanstack/octane-table' +import type { Sale } from './makeData' + +type RowSource = 'all' | 'custom' | 'filtered' | 'page' | 'selected' +type AggregationTableMeta = { rowSource: RowSource } + +const features = tableFeatures({ + rowAggregationFeature, + columnFilteringFeature, + rowPaginationFeature, + rowSelectionFeature, + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + filterFns: { includesString: filterFn_includesString }, + aggregationFns: { + count: aggregationFn_count, + extent: aggregationFn_extent, + mean: aggregationFn_mean, + sum: aggregationFn_sum, + }, + tableMeta: metaHelper(), +}) + +const columnHelper = createColumnHelper() + +function getAggregationRows(table: Table) { + const rowSource = table.options.meta?.rowSource + if (rowSource === 'all') return table.getCoreRowModel().rows + if (rowSource === 'page') return table.getRowModel().rows + if (rowSource === 'selected') return table.getFilteredSelectedRowModel().rows + if (rowSource === 'custom') return table.getCoreRowModel().rows.slice(0, 3) + return undefined +} + +function formatValue(value: unknown): string { + if (Array.isArray(value)) return value.map(formatValue).join(' – ') + if (value && typeof value === 'object') { + return Object.entries(value) + .map(([key, entry]) => `${key}: ${formatValue(entry)}`) + .join(', ') + } + if (typeof value === 'number') + return value.toLocaleString(undefined, { maximumFractionDigits: 2 }) + return String(value ?? '—') +} + +function App() @{ + const [data, setData] = useState(() => makeData(10_000)) + const [rowSource, setRowSource] = useState('filtered') + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.display({ + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ), + }), + columnHelper.accessor('category', { + header: 'Category', + filterFn: 'includesString', + }), + columnHelper.accessor('item', { + header: 'Item', + footer: ({ table }) => `${table.options.meta?.rowSource} total`, + }), + columnHelper.accessor('amount', { + header: 'Amount', + aggregationFn: 'sum', + cell: ({ getValue }) => getValue().toLocaleString(), + footer: ({ column, table }) => + formatValue( + column.getAggregationValue({ rows: getAggregationRows(table) }), + ), + }), + columnHelper.accessor('score', { + header: 'Score', + aggregationFn: [ + 'count', + 'mean', + { id: 'range', aggregationFn: 'extent' }, + ], + footer: ({ column, table }) => + formatValue( + column.getAggregationValue({ rows: getAggregationRows(table) }), + ), + }), + ]), + [], + ) + + const table = useTable( + { + features, + columns, + data, + meta: { rowSource }, + initialState: { pagination: { pageIndex: 0, pageSize: 10 } }, + debugTable: true, + debugColumns: true, + }, + (state) => state, // default selector + ) + + const refreshData = () => setData(makeData(10_000)) + const stressTest = () => setData(makeData(1_000_000)) + +
+

Aggregation without grouping

+

+ Amount uses a scalar sum. Score runs count, mean, and range + together and returns a keyed object. +

+
+ + +
+
+
+ + +
+
+ + + {table.getHeaderGroups().map((group) => ( + + {group.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getAllCells().map((cell) => ( + + ))} + + ))} + + + {table.getFooterGroups().map((group) => ( + + {group.headers.map((header) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( + + )} +
+ +
+ {header.isPlaceholder ? null : ( + + )} +
+
+
+ + + + + +
Page
+ + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + + table.setPageIndex( + event.currentTarget.value + ? Number(event.currentTarget.value) - 1 + : 0, + ) + } + className="page-size-input" + /> + + +
+
+ Showing {table.getRowModel().rows.length.toLocaleString()} of{' '} + {String(table.getRowCount().toLocaleString())} Rows +
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') +createRoot(rootElement).render(App) diff --git a/examples/octane/aggregation/src/makeData.ts b/examples/octane/aggregation/src/makeData.ts new file mode 100644 index 0000000000..ff158ed6c6 --- /dev/null +++ b/examples/octane/aggregation/src/makeData.ts @@ -0,0 +1,21 @@ +import { faker } from '@faker-js/faker' + +export type Sale = { + category: string + item: string + amount: number + score: number +} + +const categories = ['Hardware', 'Software', 'Services'] as const + +const newSale = (): Sale => ({ + category: faker.helpers.arrayElement(categories), + item: faker.commerce.productName(), + amount: faker.number.int({ min: 25, max: 5_000 }), + score: faker.number.int({ min: 60, max: 100 }), +}) + +export function makeData(length: number): Array { + return Array.from({ length }, newSale) +} diff --git a/examples/octane/aggregation/src/vite-env.d.ts b/examples/octane/aggregation/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/aggregation/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/aggregation/tests/e2e/smoke.spec.ts b/examples/octane/aggregation/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..c62167e0dd --- /dev/null +++ b/examples/octane/aggregation/tests/e2e/smoke.spec.ts @@ -0,0 +1,108 @@ +import { expect, test } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' +import path from 'node:path' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return { errors, server } +} + +function getTable(page: Page) { + return page + .locator('table:visible, .divTable:visible') + .filter({ has: page.locator('thead th, .thead .th') }) + .filter({ has: page.locator('tbody tr, .tbody .tr') }) + .first() +} + +function getHeaderCells(table: Locator) { + return table.locator('thead th, .thead .th') +} + +function getBodyRows(table: Locator) { + return table.locator('tbody tr, .tbody .tr') +} + +async function getFirstBodyRowData(table: Locator) { + const row = getBodyRows(table).first() + const firstInput = row + .locator('input:not([type="checkbox"]):not([type="radio"])') + .first() + + if ((await firstInput.count()) > 0 && (await firstInput.isVisible())) { + return firstInput.inputValue() + } + + const text = await row.textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const { errors, server } = await openExample(page) + + try { + const table = getTable(page) + const bodyRows = getBodyRows(table) + + await expect(table).toBeVisible() + await expect(getHeaderCells(table).first()).toBeVisible() + await expect(bodyRows.first()).toBeVisible() + await expect(page.getByLabel('Category filter:')).toBeVisible() + + const rowSource = page.getByLabel('Total rows:') + await expect(rowSource).toHaveValue('filtered') + await expect(rowSource.locator('option')).toHaveText([ + 'Filtered rows', + 'All rows', + 'Visible page', + 'Filtered selected rows', + 'First three core rows', + ]) + await rowSource.selectOption('all') + await expect(table.locator('tfoot')).toContainText('all total') + + const regenerateButton = page + .getByRole('button', { name: /^Regenerate Data$/i }) + .first() + + if ((await regenerateButton.count()) > 0) { + await expect(regenerateButton).toBeVisible() + + const firstRowBefore = await getFirstBodyRowData(table) + + await regenerateButton.click() + + await expect + .poll(() => getFirstBodyRowData(table)) + .not.toBe(firstRowBefore) + await expect(bodyRows.first()).toBeVisible() + } + + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/aggregation/tsconfig.json b/examples/octane/aggregation/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/aggregation/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/aggregation/vite.config.ts b/examples/octane/aggregation/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/aggregation/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/basic-dynamic-columns/index.html b/examples/octane/basic-dynamic-columns/index.html new file mode 100644 index 0000000000..ddd15fcca9 --- /dev/null +++ b/examples/octane/basic-dynamic-columns/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Octane Table + + +
+ + + diff --git a/examples/octane/basic-dynamic-columns/package.json b/examples/octane/basic-dynamic-columns/package.json new file mode 100644 index 0000000000..0760d40829 --- /dev/null +++ b/examples/octane/basic-dynamic-columns/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-octane-table-example-basic-dynamic-columns", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/basic-dynamic-columns/src/index.css b/examples/octane/basic-dynamic-columns/src/index.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/octane/basic-dynamic-columns/src/index.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/basic-dynamic-columns/src/main.tsrx b/examples/octane/basic-dynamic-columns/src/main.tsrx new file mode 100644 index 0000000000..c01f4840fd --- /dev/null +++ b/examples/octane/basic-dynamic-columns/src/main.tsrx @@ -0,0 +1,394 @@ +import { createRoot, useEffect, useMemo, useRef, useState } from 'octane' +import { + columnFacetingFeature, + columnFilteringFeature, + createFacetedMinMaxValues, + createFacetedRowModel, + createFacetedUniqueValues, + createFilteredRowModel, + createSortedRowModel, + filterFn_inNumberRange, + filterFn_includesString, + metaHelper, + rowSortingFeature, + sortFn_alphanumeric, + sortFn_basic, + sortFn_datetime, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { + Column, + ColumnDef, + FilterFn, + FilterFnOption, + OctaneTable, + SortFnOption, +} from '@tanstack/octane-table' +import './index.css' + +// This example builds its columns from the DATA instead of a hard-coded definition. +// The row shape is treated as unknown (a generic Record). For each key we: +// 1. detect the value's data type at runtime, +// 2. pick a sortFn and filterFn that suit that type, +// 3. render a different filter component per type (see the branches in ). +// The distinct values / min-max used by the filters come from the column faceting +// feature, not from a hand-rolled scan of the data. + +// 1. Treat each row as an object of unknown shape +type DynamicRow = Record + +// The runtime-detected data type for a column, stored in its meta. +type DataType = 'string' | 'number' | 'boolean' | 'date' + +// allows us to attach the detected data type to each column +interface DynamicColumnMeta { + dataType: DataType +} + +// 2. New in V9! Tell the table which features, row models, and fn registries we use. +const features = tableFeatures({ + rowSortingFeature, + columnFilteringFeature, + columnFacetingFeature, + sortedRowModel: createSortedRowModel(), + filteredRowModel: createFilteredRowModel(), + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), // powers the enum select options + facetedMinMaxValues: createFacetedMinMaxValues(), // powers the numeric range hints + sortFns: { + // register only the built-in sort fns we reference by name + alphanumeric: sortFn_alphanumeric, + basic: sortFn_basic, + datetime: sortFn_datetime, + }, + filterFns: { + // register only the built-in filter fns we reference by name + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, + columnMeta: metaHelper(), +}) + +type DynamicTable = OctaneTable + +// Custom filter fns for the data types that have no suitable built-in. +// Per convention, standalone fns use `any` for TData since they aren't shape-specific. +const booleanFilterFn: FilterFn = ( + row, + columnId, + filterValue, +) => { + if (filterValue === '' || filterValue == null) return true + return String(row.getValue(columnId)) === String(filterValue) +} + +const dateRangeFilterFn: FilterFn = ( + row, + columnId, + filterValue, +) => { + const [min, max] = (filterValue as [string, string] | undefined) ?? ['', ''] + const value = row.getValue(columnId) + const time = + value instanceof Date ? value.getTime() : new Date(String(value)).getTime() + if (min && time < new Date(min).getTime()) return false + if (max && time > new Date(max).getTime()) return false + return true +} + +// Turn a data key like "firstName" into a readable header like "First Name" +function formatHeader(key: string) { + const withSpaces = key + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .replace(/[_-]+/g, ' ') + return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1) +} + +// Inspect a sample value for a key and decide its data type. +function detectDataType(data: Array, key: string): DataType { + const sample = data.find((row) => row[key] != null)?.[key] + if (sample instanceof Date) return 'date' + if (typeof sample === 'boolean') return 'boolean' + if (typeof sample === 'number') return 'number' + return 'string' +} + +// Pick a built-in sort fn (by name) based on the data type. +function getSortFn(dataType: DataType): SortFnOption { + if (dataType === 'number' || dataType === 'boolean') return 'basic' + if (dataType === 'date') return 'datetime' + return 'alphanumeric' +} + +// Pick a filter fn based on the data type. Mixes built-in fns (by name) with +// the custom fns defined above. +function getFilterFn(dataType: DataType): FilterFnOption { + if (dataType === 'number') return 'inNumberRange' + if (dataType === 'boolean') return booleanFilterFn + if (dataType === 'date') return dateRangeFilterFn + return 'includesString' +} + +// Render a cell value based on its data type. +function renderValue(value: unknown, dataType: DataType) { + if (value == null) return '' + if (dataType === 'date') return (value as Date).toLocaleDateString() + if (dataType === 'boolean') return value ? '✅' : '❌' + return String(value) +} + +function App() @{ + const [data, setData] = useState>(() => makeData(1_000)) + const refreshData = () => setData(makeData(1_000)) + const stressTest = () => setData(makeData(1_000_000)) + + // 3. Derive the columns from the keys of the data instead of hard-coding them. + const columns = useMemo>>(() => { + if (data.length === 0) return [] + return Object.keys(data[0]!).map((key) => { + const dataType = detectDataType(data, key) + return { + accessorKey: key, + header: formatHeader(key), + meta: { dataType }, + sortFn: getSortFn(dataType), + filterFn: getFilterFn(dataType), + cell: (info) => renderValue(info.getValue(), dataType), + } + }) + }, [data]) + + // 4. Create the table instance with the derived columns and data + const table = useTable( + { + debugTable: true, + features, + columns, + data, + }, + (state) => state, // default selector + ) + +
+

+ Columns, sort fns, filter fns, and filter components are all derived + from the data type of each field, not from a hard-coded column definition. +

+
+ + +
+
+
+ + + @for (const headerGroup of table.getHeaderGroups(); key headerGroup.id) { + + @for (const header of headerGroup.headers; key header.id) { + + } + + } + + + @for (const row of table.getRowModel().rows.slice(0, 15); key row.id) { + + @for (const cell of row.getAllCells(); key cell.id) { + + } + + } + +
+ {header.isPlaceholder ? null : <> +
+ + {header.column.getIsSorted() === 'asc' + ? ' 🔼' + : header.column.getIsSorted() === 'desc' + ? ' 🔽' + : null} +
+ {header.column.getCanFilter() + ? + : null} + } +
+
+
+
{String(table.getRowModel().rows.length)} Rows
+
+} + +function Filter({ + column, + table, +}: { + column: Column + table: DynamicTable +}) @{ + // A different filter UI per data type. The reactive reads (getFilterValue, + // faceted values) go through builder-pattern APIs on a stable `column`. + // `table.Subscribe` re-runs the render prop on column-filter changes so the + // inputs stay fresh. + state.columnFilters}> + {() => } + +} + +function FilterContent({ + column, +}: { + column: Column +}) { + const { dataType } = column.columnDef.meta ?? { dataType: 'string' } + const filterValue = column.getFilterValue() + + if (dataType === 'number') { + const [min, max] = column.getFacetedMinMaxValues() ?? [] + return
+ column.setFilterValue((old: [number, number] | undefined) => [value, old?.[1]])} + placeholder={`Min${min !== undefined ? ` (${min})` : ''}`} + className="filter-input" + /> + column.setFilterValue((old: [number, number] | undefined) => [old?.[0], value])} + placeholder={`Max${max !== undefined ? ` (${max})` : ''}`} + className="filter-input" + /> +
+ } + + if (dataType === 'date') { + return
+ column.setFilterValue((old: [string, string] | undefined) => [String(value), old?.[1] ?? ''])} + className="filter-input" + /> + column.setFilterValue((old: [string, string] | undefined) => [old?.[0] ?? '', String(value)])} + className="filter-input" + /> +
+ } + + if (dataType === 'boolean') { + return + } + + const uniqueValues = Array.from(column.getFacetedUniqueValues().keys()) + .map(String) + .sort() + if (uniqueValues.length > 0 && uniqueValues.length <= 10) { + // string: low-cardinality columns become a select of their faceted values, + // everything else gets a free-text search. + return + } + + return column.setFilterValue(value)} + placeholder={`Search... (${column.getFacetedUniqueValues().size})`} + className="filter-input" + /> +} + +function EnumOptions({ values }: { values: Array }) @{ + <> + @for (const value of values; key value) { + + } + +} + +// A typical debounced input Octane component implemented with effects/timers, +// so the example exercises only the new table and store adapters. +function DebouncedInput({ + value: initialValue, + onChange, + debounce = 500, + type, + placeholder, + className, +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number + type: string + placeholder?: string + className?: string +}) @{ + const [value, setValue] = useState(initialValue) + const onChangeRef = useRef(onChange) + const timeoutRef = useRef(undefined) + onChangeRef.current = onChange + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + return () => { + if (timeoutRef.current !== undefined) { + window.clearTimeout(timeoutRef.current) + } + } + }, []) + + { + const nextValue = event.currentTarget.value + setValue(nextValue) + if (timeoutRef.current !== undefined) { + window.clearTimeout(timeoutRef.current) + } + timeoutRef.current = window.setTimeout( + () => onChangeRef.current(nextValue), + debounce, + ) + }} + /> +} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') +createRoot(rootElement).render(App) diff --git a/examples/octane/basic-dynamic-columns/src/makeData.ts b/examples/octane/basic-dynamic-columns/src/makeData.ts new file mode 100644 index 0000000000..545bdbc2d4 --- /dev/null +++ b/examples/octane/basic-dynamic-columns/src/makeData.ts @@ -0,0 +1,44 @@ +import { faker } from '@faker-js/faker' + +// The example never references these keys statically. It's typed here only so the +// generated data has a realistic mix of value types (string, enum, number, boolean, +// Date) for the dynamic column-building logic to detect at runtime. +type Person = { + firstName: string + lastName: string + status: 'relationship' | 'complicated' | 'single' + age: number + visits: number + progress: number + isActive: boolean + createdAt: Date +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + isActive: faker.datatype.boolean(), + createdAt: faker.date.past({ years: 5 }), + } +} + +export function makeData(count: number) { + return range(count).map(newPerson) +} diff --git a/examples/octane/basic-dynamic-columns/src/vite-env.d.ts b/examples/octane/basic-dynamic-columns/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/basic-dynamic-columns/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/basic-dynamic-columns/tests/e2e/smoke.spec.ts b/examples/octane/basic-dynamic-columns/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..1faebfb7a0 --- /dev/null +++ b/examples/octane/basic-dynamic-columns/tests/e2e/smoke.spec.ts @@ -0,0 +1,52 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + page.on('pageerror', (error) => errors.push(error.message)) + page.on('console', (message) => { + if (message.type() === 'error') errors.push(message.text()) + }) + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + await page.goto(server.url) + return { errors, server } +} + +test('renders generated columns and keeps sort, filter, and regeneration functional', async ({ + page, +}) => { + const { errors, server } = await openExample(page) + try { + const headers = page.locator('thead th') + await expect(headers.first()).toBeVisible() + const rows = page.locator('tbody tr') + await expect(rows).toHaveCount(15) + + const firstBeforeSort = await rows.first().innerText() + await headers.first().locator('[title="Toggle sorting"]').click() + await expect.poll(() => rows.first().innerText()).not.toBe(firstBeforeSort) + + const textFilter = page.locator('input[type="text"]').first() + if (await textFilter.count()) { + await textFilter.fill('a') + await page.waitForTimeout(600) + await expect(rows.first()).toBeVisible() + } + + const beforeRefresh = await rows.first().innerText() + await page.getByRole('button', { name: 'Regenerate Data' }).click() + await expect.poll(() => rows.first().innerText()).not.toBe(beforeRefresh) + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/basic-dynamic-columns/tsconfig.json b/examples/octane/basic-dynamic-columns/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/basic-dynamic-columns/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/basic-dynamic-columns/vite.config.ts b/examples/octane/basic-dynamic-columns/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/basic-dynamic-columns/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/basic-external-atoms/index.html b/examples/octane/basic-external-atoms/index.html new file mode 100644 index 0000000000..ddd15fcca9 --- /dev/null +++ b/examples/octane/basic-external-atoms/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Octane Table + + +
+ + + diff --git a/examples/octane/basic-external-atoms/package.json b/examples/octane/basic-external-atoms/package.json new file mode 100644 index 0000000000..193410fbdd --- /dev/null +++ b/examples/octane/basic-external-atoms/package.json @@ -0,0 +1,24 @@ +{ + "name": "tanstack-octane-table-example-basic-external-atoms", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-store": "^0.12.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/basic-external-atoms/src/index.css b/examples/octane/basic-external-atoms/src/index.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/octane/basic-external-atoms/src/index.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/basic-external-atoms/src/main.tsrx b/examples/octane/basic-external-atoms/src/main.tsrx new file mode 100644 index 0000000000..1fc03a2e96 --- /dev/null +++ b/examples/octane/basic-external-atoms/src/main.tsrx @@ -0,0 +1,158 @@ +import { createRoot, useState } from 'octane' +import { useCreateAtom } from '@tanstack/octane-store' +import { + createColumnHelper, + createPaginatedRowModel, + createSortedRowModel, + rowPaginationFeature, + rowSortingFeature, + sortFn_alphanumeric, + sortFn_text, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import type { PaginationState, SortingState } from '@tanstack/octane-table' +import { makeData } from './makeData' +import './index.css' + +type Person = { + firstName: string + lastName: string + age: number + visits: number + status: string + progress: number +} + +const features = tableFeatures({ + rowPaginationFeature, + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text }, +}) + +const columnHelper = createColumnHelper() +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { header: 'First Name', cell: (info) => String(info.getValue()) }), + columnHelper.accessor('lastName', { header: 'Last Name', cell: (info) => String(info.getValue()) }), + columnHelper.accessor('age', { header: 'Age' }), + columnHelper.accessor('visits', { header: 'Visits' }), + columnHelper.accessor('status', { header: 'Status' }), + columnHelper.accessor('progress', { header: 'Profile Progress' }), +]) +const pageSizes = [10, 20, 30, 40, 50] + +function App() @{ + const [data, setData] = useState(() => makeData(1_000)) + const refreshData = () => setData(makeData(1_000)) + const stressTest = () => setData(makeData(1_000_000)) + + // Create stable external atoms for the individual state slices you want to + // own. The table still creates internal base atoms for everything else. + const sortingAtom = useCreateAtom([]) + const paginationAtom = useCreateAtom({ + pageIndex: 0, + pageSize: 10, + }) + + // Pass the per-slice external atoms directly. The Octane adapter subscribes + // through table.store, so no separate useSelector call is needed here. + const table = useTable( + { + debugTable: true, + features, + columns, + data, + atoms: { + sorting: sortingAtom, + pagination: paginationAtom, + }, + }, + (state) => state, // default selector + ) + const paginationState = table.state.pagination + +
+
+ + +
+
+ + + @for (const headerGroup of table.getHeaderGroups(); key headerGroup.id) { + + @for (const header of headerGroup.headers; key header.id) { + + } + + } + + + @for (const row of table.getRowModel().rows; key row.id) { + + @for (const cell of row.getAllCells(); key cell.id) { + + } + + } + +
+ {header.isPlaceholder ? null :
+ + {header.column.getIsSorted() === 'asc' + ? ' 🔼' + : header.column.getIsSorted() === 'desc' + ? ' 🔽' + : null} +
} +
+
+
+ + + + + +
Page
+ {String(paginationState.pageIndex + 1)} of {String(table.getPageCount())} +
+ + | Go to page: + { + const value = event.currentTarget.value + table.setPageIndex(value ? Number(value) - 1 : 0) + }} + className="page-size-input" + /> + + +
+
+
{JSON.stringify(table.state, null, 2)}
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') +createRoot(rootElement).render(App) diff --git a/examples/octane/basic-external-atoms/src/makeData.ts b/examples/octane/basic-external-atoms/src/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/octane/basic-external-atoms/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/basic-external-atoms/src/vite-env.d.ts b/examples/octane/basic-external-atoms/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/basic-external-atoms/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/basic-external-atoms/tests/e2e/smoke.spec.ts b/examples/octane/basic-external-atoms/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..fa9bcc2272 --- /dev/null +++ b/examples/octane/basic-external-atoms/tests/e2e/smoke.spec.ts @@ -0,0 +1,65 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + page.on('pageerror', (error) => errors.push(error.message)) + page.on('console', (message) => { + if (message.type() === 'error') errors.push(message.text()) + }) + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + await page.goto(server.url) + return { errors, server } +} + +test('updates pagination atom and row model without errors', async ({ + page, +}) => { + const { errors, server } = await openExample(page) + try { + const status = page.getByText(/^Page$/).locator('..') + const first = await page.locator('tbody tr').first().innerText() + await expect(status).toContainText('1 of 100') + const timing = await page + .getByRole('button', { name: '>', exact: true }) + .evaluate((button) => { + const started = performance.now() + ;(button as HTMLButtonElement).click() + return { + elapsed: performance.now() - started, + status: document.querySelector('.controls strong')?.textContent, + } + }) + expect(timing.status).toContain('2 of 100') + expect(timing.elapsed).toBeLessThan(250) + await expect + .poll(() => page.locator('tbody tr').first().innerText()) + .not.toBe(first) + + const sortTiming = await page + .locator('.sortable-header') + .first() + .evaluate((header) => { + const started = performance.now() + ;(header as HTMLElement).click() + return { + elapsed: performance.now() - started, + text: header.textContent, + } + }) + expect(sortTiming.text).toContain('🔼') + expect(sortTiming.elapsed).toBeLessThan(250) + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/basic-external-atoms/tsconfig.json b/examples/octane/basic-external-atoms/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/basic-external-atoms/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/basic-external-atoms/vite.config.ts b/examples/octane/basic-external-atoms/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/basic-external-atoms/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/basic-external-state/index.html b/examples/octane/basic-external-state/index.html new file mode 100644 index 0000000000..ddd15fcca9 --- /dev/null +++ b/examples/octane/basic-external-state/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Octane Table + + +
+ + + diff --git a/examples/octane/basic-external-state/package.json b/examples/octane/basic-external-state/package.json new file mode 100644 index 0000000000..611e879578 --- /dev/null +++ b/examples/octane/basic-external-state/package.json @@ -0,0 +1,23 @@ +{ + "name": "tanstack-octane-table-example-basic-external-state", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/basic-external-state/src/index.css b/examples/octane/basic-external-state/src/index.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/octane/basic-external-state/src/index.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/basic-external-state/src/main.tsrx b/examples/octane/basic-external-state/src/main.tsrx new file mode 100644 index 0000000000..e96fae042f --- /dev/null +++ b/examples/octane/basic-external-state/src/main.tsrx @@ -0,0 +1,163 @@ +import { createRoot, useState } from 'octane' +import { + createColumnHelper, + createPaginatedRowModel, + createSortedRowModel, + rowPaginationFeature, + rowSortingFeature, + sortFn_alphanumeric, + sortFn_text, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import type { PaginationState, SortingState } from '@tanstack/octane-table' +import { makeData } from './makeData' +import './index.css' + +type Person = { + firstName: string + lastName: string + age: number + visits: number + status: string + progress: number +} + +const features = tableFeatures({ + rowPaginationFeature, + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text }, +}) + +const columnHelper = createColumnHelper() +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { header: 'First Name', cell: (info) => String(info.getValue()) }), + columnHelper.accessor('lastName', { header: 'Last Name', cell: (info) => String(info.getValue()) }), + columnHelper.accessor('age', { header: 'Age' }), + columnHelper.accessor('visits', { header: 'Visits' }), + columnHelper.accessor('status', { header: 'Status' }), + columnHelper.accessor('progress', { header: 'Profile Progress' }), +]) +const pageSizes = [10, 20, 30, 40, 50] + +function App() @{ + const [data, setData] = useState(() => makeData(1_000)) + const refreshData = () => setData(makeData(1_000)) + const stressTest = () => setData(makeData(1_000_000)) + + // Manage sorting state with Octane useState (component state causes more + // owner renders here than using a store-backed external atom). + const [sorting, setSorting] = useState([]) + + // Manage pagination state with Octane useState. + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }) + + // console.log('sorting', sorting) + // console.log('pagination', pagination) + + // Create the table and pass state + onChange handlers. + const table = useTable( + { + debugTable: true, + features, + columns, + data, + state: { + sorting, // connect our sorting state back down to the table + pagination, // connect our pagination state back down to the table + }, + onSortingChange: setSorting, // raise sorting state changes to our own state management + onPaginationChange: setPagination, // raise pagination state changes to our own state management + }, + (state) => state, // default selector + ) + const paginationState = table.state.pagination + +
+
+ + +
+
+ + + @for (const headerGroup of table.getHeaderGroups(); key headerGroup.id) { + + @for (const header of headerGroup.headers; key header.id) { + + } + + } + + + @for (const row of table.getRowModel().rows; key row.id) { + + @for (const cell of row.getAllCells(); key cell.id) { + + } + + } + +
+ {header.isPlaceholder ? null :
+ + {header.column.getIsSorted() === 'asc' + ? ' 🔼' + : header.column.getIsSorted() === 'desc' + ? ' 🔽' + : null} +
} +
+
+
+ + + + + +
Page
+ {String(paginationState.pageIndex + 1)} of {String(table.getPageCount())} +
+ + | Go to page: + { + const value = event.currentTarget.value + table.setPageIndex(value ? Number(value) - 1 : 0) + }} + className="page-size-input" + /> + + +
+
+
{JSON.stringify(table.state, null, 2)}
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') +createRoot(rootElement).render(App) diff --git a/examples/octane/basic-external-state/src/makeData.ts b/examples/octane/basic-external-state/src/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/octane/basic-external-state/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/basic-external-state/src/vite-env.d.ts b/examples/octane/basic-external-state/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/basic-external-state/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/basic-external-state/tests/e2e/smoke.spec.ts b/examples/octane/basic-external-state/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..d8c7294f12 --- /dev/null +++ b/examples/octane/basic-external-state/tests/e2e/smoke.spec.ts @@ -0,0 +1,73 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + page.on('pageerror', (error) => errors.push(error.message)) + page.on('console', (message) => { + if (message.type() === 'error') errors.push(message.text()) + }) + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + await page.goto(server.url) + return { errors, server } +} + +async function firstRow(page: Page) { + return (await page.locator('tbody tr').first().innerText()).replace( + /\s+/g, + ' ', + ) +} + +test('advances controlled pagination repeatedly without render-phase errors', async ({ + page, +}) => { + const { errors, server } = await openExample(page) + try { + const next = page.getByRole('button', { name: '>', exact: true }) + const status = page.getByText(/^Page$/).locator('..') + await expect(status).toContainText('1 of 100') + const first = await firstRow(page) + const timing = await next.evaluate((button) => { + const started = performance.now() + ;(button as HTMLButtonElement).click() + return { + elapsed: performance.now() - started, + status: document.querySelector('.controls strong')?.textContent, + } + }) + expect(timing.status).toContain('2 of 100') + expect(timing.elapsed).toBeLessThan(250) + await expect.poll(() => firstRow(page)).not.toBe(first) + const second = await firstRow(page) + await next.click() + await expect(status).toContainText('3 of 100') + await expect.poll(() => firstRow(page)).not.toBe(second) + + const sortTiming = await page + .locator('.sortable-header') + .first() + .evaluate((header) => { + const started = performance.now() + ;(header as HTMLElement).click() + return { + elapsed: performance.now() - started, + text: header.textContent, + } + }) + expect(sortTiming.text).toContain('🔼') + expect(sortTiming.elapsed).toBeLessThan(250) + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/basic-external-state/tsconfig.json b/examples/octane/basic-external-state/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/basic-external-state/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/basic-external-state/vite.config.ts b/examples/octane/basic-external-state/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/basic-external-state/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/basic-subscribe/index.html b/examples/octane/basic-subscribe/index.html new file mode 100644 index 0000000000..ddd15fcca9 --- /dev/null +++ b/examples/octane/basic-subscribe/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Octane Table + + +
+ + + diff --git a/examples/octane/basic-subscribe/package.json b/examples/octane/basic-subscribe/package.json new file mode 100644 index 0000000000..d527df1b64 --- /dev/null +++ b/examples/octane/basic-subscribe/package.json @@ -0,0 +1,24 @@ +{ + "name": "tanstack-octane-table-example-basic-subscribe", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-store": "^0.12.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/basic-subscribe/src/index.css b/examples/octane/basic-subscribe/src/index.css new file mode 100644 index 0000000000..0a099f6d9f --- /dev/null +++ b/examples/octane/basic-subscribe/src/index.css @@ -0,0 +1,372 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled, +button[disabled] { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/basic-subscribe/src/main.tsrx b/examples/octane/basic-subscribe/src/main.tsrx new file mode 100644 index 0000000000..047dc5351c --- /dev/null +++ b/examples/octane/basic-subscribe/src/main.tsrx @@ -0,0 +1,412 @@ +import { + createRoot, + useEffect, + useMemo, + useRef, + useState, +} from 'octane' +import { useCreateAtom } from '@tanstack/octane-store' +import { + Subscribe, + columnFilteringFeature, + createColumnHelper, + createFilteredRowModel, + createPaginatedRowModel, + filterFn_inNumberRange, + filterFn_includesString, + globalFilteringFeature, + rowPaginationFeature, + rowSelectionFeature, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { + Column, + OctaneTable, + PaginationState, + Row, + RowSelectionState, + TableState, +} from '@tanstack/octane-table' +import type { Person } from './makeData' +import './index.css' + +const features = tableFeatures({ + rowPaginationFeature, + rowSelectionFeature, + columnFilteringFeature, + globalFilteringFeature, + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +type TableInstance = OctaneTable +const columnHelper = createColumnHelper() +const pageSizes = [10, 20, 30, 40, 50] + +/** + * This is an example showing how to use advanced re-rendering optimizations with more fine-grained control over what is subscribed to. + * Subscribe/table.Subscribe is a higher-order component that allows you to subscribe to the table state or individual atoms/stores. + * This is useful for making sure that re-renders only happen at certain parts of the Octane tree exactly where they need to be. + * We recommend only using these patterns when you run into specific performance issues. + */ +function App() @{ + // Column definitions have a stable reference. + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.display({ + id: 'select', + // Just import Subscribe if an Octane table is not available in scope and pass the table store as source. + // It can be difficult to know what to subscribe to unless you understand every state dependency of the internal APIs. + // Be careful to test and validate that it re-renders when you expect it to. + header: ({ table }) => state.rowSelection} + > + {() => } + , + cell: ({ row, table }) => , + }), + columnHelper.accessor('firstName', { + header: 'First Name', + cell: (info) => String(info.getValue()), + }), + columnHelper.accessor('lastName', { + header: 'Last Name', + cell: (info) => String(info.getValue()), + }), + columnHelper.accessor('age', { header: 'Age' }), + columnHelper.accessor('visits', { header: 'Visits' }), + columnHelper.accessor('status', { header: 'Status' }), + columnHelper.accessor('progress', { header: 'Profile Progress' }), + ]), + [], + ) + const [data, setData] = useState(() => makeData(1_000)) + const refreshData = () => setData(makeData(1_000)) + const stressTest = () => setData(makeData(1_000_000)) + + // optionally, raise the selection state to your own atom + const rowSelectionAtom = useCreateAtom({}) + + const table = useTable( + { + debugTable: true, + features, + atoms: { rowSelection: rowSelectionAtom }, + columns, + data, + getRowId: (row) => row.id, + enableRowSelection: true, // enable row selection for all rows + // enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row + }, + () => null, // subscribe to no table state by default; use table.Subscribe below for targeted updates + ) + +
+
+ + +
+ {/* Identity-source subscription: only the global-filter island rerenders. */} + + {(globalFilter: string | undefined) => table.setGlobalFilter(value)} + className="summary-panel" + placeholder="Search all columns..." + />} + +
+ + + @for (const headerGroup of table.getHeaderGroups(); key headerGroup.id) { + + @for (const header of headerGroup.headers; key header.id) { + + } + + } + + + {/* Subscribe the row model to filtering and pagination only. Row selection is handled per row below. */} + ({ + columnFilters: state.columnFilters, + globalFilter: state.globalFilter, + pagination: state.pagination, + })} + > + {() => } + + + + + + + + +
+ {header.isPlaceholder ? null : <> + + {header.column.getCanFilter() + ?
+ : null} + } +
+ + {() => } + + + Page Rows ({String(table.getRowModel().rows.length)}) +
+
+ state.pagination}> + {(pagination: PaginationState) =>
+ + + + + +
Page
+ {String(pagination.pageIndex + 1)} of {String(table.getPageCount())} +
+ + | Go to page: + { + const value = event.currentTarget.value + table.setPageIndex(value ? Number(value) - 1 : 0) + }} + className="page-size-input" + /> + + +
} +
+ {/* Subscribe to the entire row-selection atom. */} + + {(rowSelection: RowSelectionState) =>
+ {String(Object.keys(rowSelection).length)} of {String(table.getPreFilteredRowModel().rows.length)} Total Rows Selected +
} +
+
+
+
+ +
+
+ + {/* Subscribe to the entire table state with the standalone component. */} + state}> + {(state: TableState) =>
{JSON.stringify(state, null, 2)}
} +
+
+
+} + +function SubscribedRows({ table }: { table: TableInstance }) @{ + <> + @for (const row of table.getRowModel().rows; key row.id) { + + @for (const cell of row.getAllCells(); key cell.id) { + + } + + } + +} + +function Filter({ + column, + table, +}: { + column: Column + table: TableInstance +}) @{ + const firstValue = table + .getPreFilteredRowModel() + .flatRows[0]?.getValue(column.id) + + + {() => typeof firstValue === 'number' + ?
+ column.setFilterValue( + (old: [number, number] | undefined) => [value, old?.[1]], + )} + placeholder="Min" + className="filter-input" + /> + column.setFilterValue( + (old: [number, number] | undefined) => [old?.[0], value], + )} + placeholder="Max" + className="filter-input" + /> +
+ : column.setFilterValue(value)} + placeholder="Search..." + className="filter-select" + />} +
+} + +function RowCheckbox({ + row, + table, +}: { + row: Row + table: TableInstance +}) @{ + // Select only this row's selection value so toggling one row only re-renders that row's checkbox. + Boolean(rowSelection[row.id])} // optimize to only re-render when the row selection changes for this row + > + {(selected: boolean) => } + +} + +// A debounced input Octane component +function DebouncedInput({ + value: initialValue, + onChange, + debounce = 500, + placeholder, + className, + ariaLabel, + type = 'text', +}: { + value: string | number + onChange: (value: string) => void + debounce?: number + placeholder?: string + className?: string + ariaLabel: string + type?: string +}) @{ + const [value, setValue] = useState(initialValue) + const onChangeRef = useRef(onChange) + const timeoutRef = useRef(undefined) + onChangeRef.current = onChange + + useEffect(() => setValue(initialValue), [initialValue]) + useEffect(() => { + return () => { + if (timeoutRef.current !== undefined) { + window.clearTimeout(timeoutRef.current) + } + } + }, []) + + { + const nextValue = event.currentTarget.value + setValue(nextValue) + if (timeoutRef.current !== undefined) { + window.clearTimeout(timeoutRef.current) + } + timeoutRef.current = window.setTimeout( + () => onChangeRef.current(nextValue), + debounce, + ) + }} + /> +} + +function IndeterminateCheckbox({ + indeterminate, + checked, + disabled, + onChange, + ariaLabel, +}: { + indeterminate?: boolean + checked?: boolean + disabled?: boolean + onChange?: (event: Event) => void + ariaLabel: string +}) @{ + const ref = useRef(null) + + useEffect(() => { + if (ref.current) ref.current.indeterminate = !checked && Boolean(indeterminate) + }, [indeterminate, checked]) + + +} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') +createRoot(rootElement).render(App) diff --git a/examples/octane/basic-subscribe/src/makeData.ts b/examples/octane/basic-subscribe/src/makeData.ts new file mode 100644 index 0000000000..c34c43a03e --- /dev/null +++ b/examples/octane/basic-subscribe/src/makeData.ts @@ -0,0 +1,50 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + id: string + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + id: faker.string.uuid(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/basic-subscribe/src/vite-env.d.ts b/examples/octane/basic-subscribe/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/basic-subscribe/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/basic-subscribe/tests/e2e/smoke.spec.ts b/examples/octane/basic-subscribe/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..6ab29387f5 --- /dev/null +++ b/examples/octane/basic-subscribe/tests/e2e/smoke.spec.ts @@ -0,0 +1,53 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + page.on('pageerror', (error) => errors.push(error.message)) + page.on('console', (message) => { + if (message.type() === 'error') errors.push(message.text()) + }) + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + await page.goto(server.url) + return { errors, server } +} + +test('updates global-filter, projected, identity, and whole-store islands independently', async ({ + page, +}) => { + const { errors, server } = await openExample(page) + try { + const rows = page.locator('[data-testid="row-model-subscription"] tr') + await expect(rows).toHaveCount(10) + + const globalFilter = page.getByRole('textbox', { name: 'Global filter' }) + await globalFilter.fill('tanner') + await expect.poll(() => rows.count()).toBeLessThan(10) + await globalFilter.fill('') + await expect(rows).toHaveCount(10) + + const rowCheckbox = page + .getByRole('checkbox', { name: /^Select row / }) + .first() + await rowCheckbox.check() + await expect( + page.locator('[data-testid="identity-source-subscription"]'), + ).toContainText('1 of') + await expect( + page.locator('[data-testid="whole-store-subscription"]'), + ).toContainText('"rowSelection"') + await expect(rowCheckbox).toBeChecked() + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/basic-subscribe/tsconfig.json b/examples/octane/basic-subscribe/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/basic-subscribe/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/basic-subscribe/vite.config.ts b/examples/octane/basic-subscribe/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/basic-subscribe/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/basic-use-app-table/index.html b/examples/octane/basic-use-app-table/index.html new file mode 100644 index 0000000000..ddd15fcca9 --- /dev/null +++ b/examples/octane/basic-use-app-table/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Octane Table + + +
+ + + diff --git a/examples/octane/basic-use-app-table/package.json b/examples/octane/basic-use-app-table/package.json new file mode 100644 index 0000000000..78c4eba66a --- /dev/null +++ b/examples/octane/basic-use-app-table/package.json @@ -0,0 +1,22 @@ +{ + "name": "tanstack-octane-table-example-basic-use-app-table", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/basic-use-app-table/src/index.css b/examples/octane/basic-use-app-table/src/index.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/octane/basic-use-app-table/src/index.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/basic-use-app-table/src/main.tsrx b/examples/octane/basic-use-app-table/src/main.tsrx new file mode 100644 index 0000000000..2d725ce9a7 --- /dev/null +++ b/examples/octane/basic-use-app-table/src/main.tsrx @@ -0,0 +1,126 @@ +import { createRoot, useState } from 'octane' +import { createTableHook } from '@tanstack/octane-table' +import './index.css' + +// This example uses the new `createTableHook` method to create a re-usable table hook factory instead of independently using the standalone `useTable` hook and `createColumnHelper` method. You can choose to use either way. + +// 1. Define what the shape of your data will be for each row +type Person = { + firstName: string + lastName: string + age: number + visits: number + status: string + progress: number +} + +// 2. Create some dummy data with a stable reference (this could be an API response stored in useState or similar) +const defaultData: Array = [ + { firstName: 'tanner', lastName: 'linsley', age: 24, visits: 100, status: 'In Relationship', progress: 50 }, + { firstName: 'tandy', lastName: 'miller', age: 40, visits: 40, status: 'Single', progress: 80 }, + { firstName: 'joe', lastName: 'dirte', age: 45, visits: 20, status: 'Complicated', progress: 10 }, + { firstName: 'kevin', lastName: 'vandy', age: 28, visits: 100, status: 'Single', progress: 70 }, +] + +function CellValue() @{ + const cell = useCellContext() + {String(cell.renderValue())} +} + +function HeaderValue() @{ + const header = useHeaderContext() + {String(header.column.id)} +} + +// 3. New in V9! Tell the table which features and row models we want to use. In this case, this will be a basic table with no additional features +const { + useAppTable, + createAppColumnHelper, + useCellContext, + useHeaderContext, +} = createTableHook({ + features: {}, + debugTable: true, + cellComponents: { CellValue }, + headerComponents: { HeaderValue }, +}) + +// 4. Create a helper object to help define our columns +const columnHelper = createAppColumnHelper() + +// 5. Define the columns for your table with a stable reference (in this case, defined statically outside of an Octane component) +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { header: 'First Name', footer: (info) => info.column.id }), + columnHelper.accessor((row) => row.lastName, { id: 'lastName', header: 'Last Name', footer: (info) => info.column.id }), + columnHelper.accessor((row) => Number(row.age), { id: 'age', header: 'Age', footer: (info) => info.column.id }), + columnHelper.accessor('visits', { header: 'Visits', footer: (info) => info.column.id }), + columnHelper.accessor('status', { header: 'Status', footer: (info) => info.column.id }), + columnHelper.accessor('progress', { header: 'Profile Progress', footer: (info) => info.column.id }), +]) + +function App() @{ + // 6. Store data with a stable reference + const [data] = useState(() => [...defaultData]) + + // 7. Create the table instance with the required columns and data. + // Features and row models are already defined in the createTableHook call above. + const table = useAppTable( + { + debugTable: true, + columns, + data, + // add additional table options here or in the createTableHook call above + }, + (state) => state, // default selector + ) + + // 8. Render your table markup from the table instance APIs + +
+ + + @for (const headerGroup of table.getHeaderGroups(); key headerGroup.id) { + + @for (const header of headerGroup.headers; key header.id) { + + {(appHeader) => } + + } + + } + + + @for (const row of table.getRowModel().rows; key row.id) { + + @for (const cell of row.getAllCells(); key cell.id) { + + {(appCell) => } + + } + + } + + + @for (const footerGroup of table.getFooterGroups(); key footerGroup.id) { + + @for (const header of footerGroup.headers; key header.id) { + + {(appFooter) => } + + } + + } + +
+ {header.isPlaceholder ? null : } + +
+ +
+
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') +createRoot(rootElement).render(App) diff --git a/examples/octane/basic-use-app-table/src/vite-env.d.ts b/examples/octane/basic-use-app-table/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/basic-use-app-table/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/basic-use-app-table/tests/e2e/smoke.spec.ts b/examples/octane/basic-use-app-table/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..edcefcec06 --- /dev/null +++ b/examples/octane/basic-use-app-table/tests/e2e/smoke.spec.ts @@ -0,0 +1,37 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + page.on('pageerror', (error) => errors.push(error.message)) + page.on('console', (message) => { + if (message.type() === 'error') errors.push(message.text()) + }) + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + await page.goto(server.url) + return { errors, server } +} + +test('renders registered composition components and context renderers', async ({ + page, +}) => { + const { errors, server } = await openExample(page) + try { + await expect(page.locator('[data-registered-header]')).toHaveCount(6) + await expect(page.locator('[data-registered-cell]')).toHaveCount(24) + await expect(page.locator('tbody td').first()).toContainText('tanner') + await expect(page.locator('tfoot th').first()).toContainText('firstName') + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/basic-use-app-table/tsconfig.json b/examples/octane/basic-use-app-table/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/basic-use-app-table/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/basic-use-app-table/vite.config.ts b/examples/octane/basic-use-app-table/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/basic-use-app-table/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/basic-use-table/index.html b/examples/octane/basic-use-table/index.html new file mode 100644 index 0000000000..ddd15fcca9 --- /dev/null +++ b/examples/octane/basic-use-table/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Octane Table + + +
+ + + diff --git a/examples/octane/basic-use-table/package.json b/examples/octane/basic-use-table/package.json new file mode 100644 index 0000000000..8e1a6c9500 --- /dev/null +++ b/examples/octane/basic-use-table/package.json @@ -0,0 +1,22 @@ +{ + "name": "tanstack-octane-table-example-basic-use-table", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/basic-use-table/src/index.css b/examples/octane/basic-use-table/src/index.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/octane/basic-use-table/src/index.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/basic-use-table/src/main.tsrx b/examples/octane/basic-use-table/src/main.tsrx new file mode 100644 index 0000000000..26b61356d6 --- /dev/null +++ b/examples/octane/basic-use-table/src/main.tsrx @@ -0,0 +1,116 @@ +import { createRoot, useState } from 'octane' +import { tableFeatures, useTable } from '@tanstack/octane-table' +import type { ColumnDef } from '@tanstack/octane-table' +import './index.css' + +// This example uses the classic standalone `useTable` hook to create a table without the new `createTableHelper` util. + +// 1. Define what the shape of your data will be for each row +type Person = { + firstName: string + lastName: string + age: number + visits: number + status: string + progress: number +} + +// 2. Create some dummy data with a stable reference (this could be an API response stored in useState or similar) +const defaultData: Array = [ + { firstName: 'tanner', lastName: 'linsley', age: 24, visits: 100, status: 'In Relationship', progress: 50 }, + { firstName: 'tandy', lastName: 'miller', age: 40, visits: 40, status: 'Single', progress: 80 }, + { firstName: 'joe', lastName: 'dirte', age: 45, visits: 20, status: 'Complicated', progress: 10 }, + { firstName: 'kevin', lastName: 'vandy', age: 12, visits: 100, status: 'Single', progress: 70 }, +] + +// 3. New in V9! Tell the table which features and row models we want to use. In this case, this will be a basic table with no additional features +const features = tableFeatures({}) + +// 4. Define the columns for your table. This uses the new `ColumnDef` type to define columns. Alternatively, check out the createTableHelper/createColumnHelper util for an even more type-safe way to define columns. +const columns: Array> = [ + { + // accessorKey method (most common for simple use-cases) + accessorKey: 'firstName', + header: 'First Name', + cell: (info) => String(info.getValue()), + }, + { + // accessorFn used (alternative) along with a custom id + accessorFn: (row) => row.lastName, + id: 'lastName', + header: () => Last Name, + cell: (info) => {String(info.getValue())}, + }, + { + // accessorFn used to transform the data + accessorFn: (row) => Number(row.age), + id: 'age', + header: () => 'Age', + cell: (info) => String(info.renderValue()), + }, + { + accessorKey: 'visits', + header: () => Visits, + }, + { accessorKey: 'status', header: 'Status' }, + { accessorKey: 'progress', header: 'Profile Progress' }, +] + +function App() @{ + // 5. Store data with a stable reference + const [data] = useState(() => [...defaultData]) + + // 6. Create the table instance with required features, columns, and data + const table = useTable( + { + debugTable: true, // optionally, enable console logging debug messages + features, // new required option in V9. Tell the table which features you are importing and using (better tree-shaking) + columns, + data, + // add additional table options here + }, + (state) => state, // default selector + ) + + // 7. Render your table markup from the table instance APIs +
+ + + @for (const headerGroup of table.getHeaderGroups(); key headerGroup.id) { + + @for (const header of headerGroup.headers; key header.id) { + + } + + } + + + @for (const row of table.getRowModel().rows; key row.id) { + + @for (const cell of row.getAllCells(); key cell.id) { + + } + + } + + + @for (const footerGroup of table.getFooterGroups(); key footerGroup.id) { + + @for (const header of footerGroup.headers; key header.id) { + + } + + } + +
+ {header.isPlaceholder ? null : } +
+ {header.isPlaceholder ? null : } +
+
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') +createRoot(rootElement).render(App) diff --git a/examples/octane/basic-use-table/src/vite-env.d.ts b/examples/octane/basic-use-table/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/basic-use-table/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/basic-use-table/tests/e2e/smoke.spec.ts b/examples/octane/basic-use-table/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..d782a3fc43 --- /dev/null +++ b/examples/octane/basic-use-table/tests/e2e/smoke.spec.ts @@ -0,0 +1,39 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + page.on('pageerror', (error) => errors.push(error.message)) + page.on('console', (message) => { + if (message.type() === 'error') errors.push(message.text()) + }) + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + await page.goto(server.url) + return { errors, server } +} + +test('renders headers, rows, cells, and footers without errors', async ({ + page, +}) => { + const { errors, server } = await openExample(page) + try { + const table = page.locator('table') + await expect(table).toBeVisible() + await expect(table.locator('thead th')).toHaveCount(6) + await expect(table.locator('tbody tr')).toHaveCount(4) + await expect(table.locator('tbody td').first()).toHaveText('tanner') + await expect(table.locator('tfoot')).toBeVisible() + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/basic-use-table/tsconfig.json b/examples/octane/basic-use-table/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/basic-use-table/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/basic-use-table/vite.config.ts b/examples/octane/basic-use-table/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/basic-use-table/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/column-ordering/index.html b/examples/octane/column-ordering/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/column-ordering/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/column-ordering/package.json b/examples/octane/column-ordering/package.json new file mode 100644 index 0000000000..44c4cc1c47 --- /dev/null +++ b/examples/octane/column-ordering/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-column-ordering", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/column-ordering/src/index.css b/examples/octane/column-ordering/src/index.css new file mode 100644 index 0000000000..efd0b26417 --- /dev/null +++ b/examples/octane/column-ordering/src/index.css @@ -0,0 +1,374 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td:last-child { + border-right: 0; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/column-ordering/src/main.tsrx b/examples/octane/column-ordering/src/main.tsrx new file mode 100644 index 0000000000..fc0ebfda87 --- /dev/null +++ b/examples/octane/column-ordering/src/main.tsrx @@ -0,0 +1,194 @@ +import { useState } from 'octane' +import { createRoot } from 'octane' +import { faker } from '@faker-js/faker' +import { + columnOrderingFeature, + columnVisibilityFeature, + createColumnHelper, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Person } from './makeData' +import './index.css' + +const features = tableFeatures({ + columnOrderingFeature, + columnVisibilityFeature, +}) + +const columnHelper = createColumnHelper() + +const defaultColumns = columnHelper.columns([ + columnHelper.group({ + header: 'Name', + footer: (props) => props.column.id, + columns: columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + footer: (props) => props.column.id, + }), + ]), + }), + columnHelper.group({ + header: 'Info', + footer: (props) => props.column.id, + columns: columnHelper.columns([ + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + }), + columnHelper.group({ + header: 'More Info', + columns: columnHelper.columns([ + columnHelper.accessor('visits', { + header: () => Visits, + footer: (props) => props.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + }), + ]), + }), + ]), + }), +]) + +function App() @{ + const [data, setData] = useState(() => makeData(20)) + const [columns] = useState(() => [...defaultColumns]) + + const refreshData = () => setData(() => makeData(20)) + const stressTest = () => setData(() => makeData(1_000)) + + const table = useTable( + { + features, + columns, + data, + // initialState: { columnOrder: ['lastName', 'firstName'] }, // set column order on first render + // atoms: { columnOrder: columnOrderAtom }, // preferred: own ordering state with an external atom + // state: { columnOrder }, // classic controlled state; pair with onColumnOrderChange + // onColumnOrderChange: setColumnOrder, + debugTable: true, + debugHeaders: true, + debugColumns: true, + }, + (state) => state, // default selector + ) + + const randomizeColumns = () => { + table.setColumnOrder( + faker.helpers.shuffle(table.getAllLeafColumns().map((d) => d.id)), + ) + } + +
+
+
+ +
+ {table.getAllLeafColumns().map((column) => { + return ( +
+ +
+ ) + })} +
+
+
+ + + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + + + {table.getFooterGroups().map((footerGroup) => ( + + {footerGroup.headers.map((header) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( + + )} +
+ +
+ {header.isPlaceholder ? null : ( + + )} +
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/column-ordering/src/makeData.ts b/examples/octane/column-ordering/src/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/octane/column-ordering/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/column-ordering/src/vite-env.d.ts b/examples/octane/column-ordering/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/column-ordering/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/column-ordering/tests/e2e/smoke.spec.ts b/examples/octane/column-ordering/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..97fcb92771 --- /dev/null +++ b/examples/octane/column-ordering/tests/e2e/smoke.spec.ts @@ -0,0 +1,208 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +/** Leaf columns in their declared order. The toggle panel labels each by id. */ +const LEAF_COLUMNS = [ + 'firstName', + 'lastName', + 'age', + 'visits', + 'status', + 'progress', +] + +let server: Awaited> | undefined + +test.beforeAll(async () => { + // One server for the whole file. A cold Vite start costs more than a test. + test.setTimeout(180_000) + server = await startExampleServer(exampleDir) +}) + +test.afterAll(async () => { + await server?.close() +}) + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + if (!server) throw new Error('Example server failed to start') + + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return errors +} + +function shuffleButton(page: Page) { + return page.getByRole('button', { name: 'Shuffle Columns' }) +} + +function columnToggle(page: Page, label: string) { + return page.getByLabel(label, { exact: true }) +} + +/** Header rows run group to leaf, so the leaf row holds the visible columns. */ +function leafHeaderCells(page: Page) { + return page.locator('thead tr').last().locator('th') +} + +async function readHeaderOrder(page: Page) { + const texts = await leafHeaderCells(page).allTextContents() + return texts.map((text) => text.trim()) +} + +/** Row data is random faker output, so `table.state` is the stable oracle. */ +async function readState(page: Page) { + const text = await page.getByTestId('table-state').textContent() + + return JSON.parse(text ?? '{}') as { + columnOrder?: Array + columnVisibility?: Record + } +} + +async function getFirstBodyRowText(page: Page) { + const text = await page.locator('tbody tr').first().textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const errors = await openExample(page) + const table = page.locator('table').first() + + await expect(table).toBeVisible() + await expect(leafHeaderCells(page)).toHaveCount(LEAF_COLUMNS.length) + await expect(page.locator('tbody tr')).toHaveCount(20) + await expect(shuffleButton(page)).toBeVisible() + // No explicit order yet, so the columns render as declared. + await expect + .poll(async () => (await readState(page)).columnOrder ?? []) + .toEqual([]) + + expect(errors).toEqual([]) +}) + +test('regenerates table data', async ({ page }) => { + const errors = await openExample(page) + const regenerateButton = page.getByRole('button', { + name: /^Regenerate Data$/i, + }) + + await expect(page.locator('tbody tr').first()).toBeVisible() + + const firstRowBefore = await getFirstBodyRowText(page) + + await regenerateButton.click() + + await expect.poll(() => getFirstBodyRowText(page)).not.toBe(firstRowBefore) + await expect(page.locator('tbody tr')).toHaveCount(20) + + expect(errors).toEqual([]) +}) + +test('shuffles the columns into a new order', async ({ page }) => { + const errors = await openExample(page) + const before = await readHeaderOrder(page) + + await shuffleButton(page).click() + + await expect + .poll(async () => (await readState(page)).columnOrder ?? []) + .toHaveLength(LEAF_COLUMNS.length) + + const after = await readHeaderOrder(page) + + // Shuffling permutes the columns: same set, different sequence. + expect([...after].sort()).toEqual([...before].sort()) + expect(after).not.toEqual(before) + + const order = (await readState(page)).columnOrder ?? [] + expect([...order].sort()).toEqual([...LEAF_COLUMNS].sort()) + + expect(errors).toEqual([]) +}) + +test('reorders the body cells to match the headers', async ({ page }) => { + const errors = await openExample(page) + + await shuffleButton(page).click() + await expect + .poll(async () => (await readState(page)).columnOrder ?? []) + .toHaveLength(LEAF_COLUMNS.length) + + const order = (await readState(page)).columnOrder ?? [] + const footerOrder = ( + await page.locator('tfoot tr').first().locator('th').allTextContents() + ).map((text) => text.trim()) + + // The footer renders raw column ids, so it must match the shuffled order + // exactly. That proves the cells moved with their headers. + expect(footerOrder).toEqual(order) + + expect(errors).toEqual([]) +}) + +test('keeps the shuffled order when a column is hidden', async ({ page }) => { + const errors = await openExample(page) + + await shuffleButton(page).click() + await expect + .poll(async () => (await readState(page)).columnOrder ?? []) + .toHaveLength(LEAF_COLUMNS.length) + + const order = (await readState(page)).columnOrder ?? [] + + await columnToggle(page, 'age').uncheck() + + await expect + .poll(async () => (await readState(page)).columnVisibility ?? {}) + .toEqual({ age: false }) + await expect(leafHeaderCells(page)).toHaveCount(LEAF_COLUMNS.length - 1) + + // Hiding removes a column from the render without disturbing the rest. + const footerOrder = ( + await page.locator('tfoot tr').first().locator('th').allTextContents() + ).map((text) => text.trim()) + expect(footerOrder).toEqual(order.filter((id) => id !== 'age')) + + expect(errors).toEqual([]) +}) + +test('keeps the shuffled order when data is regenerated', async ({ page }) => { + const errors = await openExample(page) + + await shuffleButton(page).click() + await expect + .poll(async () => (await readState(page)).columnOrder ?? []) + .toHaveLength(LEAF_COLUMNS.length) + + const order = await readHeaderOrder(page) + + // Column order lives in table state, so replacing the rows must not reset it. + await page.getByRole('button', { name: /^Regenerate Data$/i }).click() + + await expect(page.locator('tbody tr')).toHaveCount(20) + expect(await readHeaderOrder(page)).toEqual(order) + + expect(errors).toEqual([]) +}) diff --git a/examples/octane/column-ordering/tsconfig.json b/examples/octane/column-ordering/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/column-ordering/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/column-ordering/vite.config.ts b/examples/octane/column-ordering/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/column-ordering/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/column-pinning-split/index.html b/examples/octane/column-pinning-split/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/column-pinning-split/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/column-pinning-split/package.json b/examples/octane/column-pinning-split/package.json new file mode 100644 index 0000000000..d3d79a66cb --- /dev/null +++ b/examples/octane/column-pinning-split/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-column-pinning-split", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/column-pinning-split/src/index.css b/examples/octane/column-pinning-split/src/index.css new file mode 100644 index 0000000000..efd0b26417 --- /dev/null +++ b/examples/octane/column-pinning-split/src/index.css @@ -0,0 +1,374 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td:last-child { + border-right: 0; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/column-pinning-split/src/main.tsrx b/examples/octane/column-pinning-split/src/main.tsrx new file mode 100644 index 0000000000..4676df8ddd --- /dev/null +++ b/examples/octane/column-pinning-split/src/main.tsrx @@ -0,0 +1,370 @@ +import { useState } from 'octane' +import { createRoot } from 'octane' +import { faker } from '@faker-js/faker' +import './index.css' +import { + columnOrderingFeature, + columnPinningFeature, + columnVisibilityFeature, + createColumnHelper, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Person } from './makeData' + +const features = tableFeatures({ + columnVisibilityFeature, + columnPinningFeature, + columnOrderingFeature, +}) + +const columnHelper = createColumnHelper() +const defaultColumns = columnHelper.columns([ + columnHelper.group({ + header: 'Name', + footer: (props) => props.column.id, + columns: columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + footer: (props) => props.column.id, + }), + ]), + }), + columnHelper.group({ + header: 'Info', + footer: (props) => props.column.id, + columns: columnHelper.columns([ + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + }), + columnHelper.group({ + header: 'More Info', + columns: columnHelper.columns([ + columnHelper.accessor('visits', { + header: () => Visits, + footer: (props) => props.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + }), + ]), + }), + ]), + }), +]) + +function App() @{ + const [data, setData] = useState(() => makeData(1_000)) + const [columns] = useState(() => [...defaultColumns]) + + const refreshData = () => setData(() => makeData(1_000)) + const stressTest = () => setData(() => makeData(1_000_000)) + + const table = useTable( + { + features, + columns, + data, + // initialState: { columnPinning: { start: ['firstName'], end: [] } }, // `start`/`end` follow layout direction + // atoms: { columnPinning: columnPinningAtom }, // preferred: own pinning state with an external atom + // state: { columnPinning }, // classic controlled state; pair with onColumnPinningChange + // onColumnPinningChange: setColumnPinning, + // enableColumnPinning: false, // disable pinning for every column; default true + debugTable: true, + debugHeaders: true, + debugColumns: true, + }, + (state) => state, // default selector + ) + + const randomizeColumns = () => { + table.setColumnOrder( + faker.helpers.shuffle(table.getAllLeafColumns().map((d) => d.id)), + ) + } + +
+
+
+ +
+ {table.getAllLeafColumns().map((column) => { + return ( +
+ +
+ ) + })} +
+
+
+ + + +
+
+

+ This example takes advantage of the "splitting" APIs. (APIs that have + "left, "center", and "right" modifiers) +

+
+ + + {table.getStartHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table + .getRowModel() + .rows.slice(0, 20) + .map((row) => { + return ( + + {row.getStartVisibleCells().map((cell) => { + return ( + + ) + })} + + ) + })} + +
+
+ {header.isPlaceholder ? null : ( + + )} +
+ {!header.isPlaceholder && header.column.getCanPin() && ( +
+ {header.column.getIsPinned() !== 'start' ? ( + + ) : null} + {header.column.getIsPinned() ? ( + + ) : null} + {header.column.getIsPinned() !== 'end' ? ( + + ) : null} +
+ )} +
+ +
+ + + {table.getCenterHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table + .getRowModel() + .rows.slice(0, 20) + .map((row) => { + return ( + + {row.getCenterVisibleCells().map((cell) => { + return ( + + ) + })} + + ) + })} + +
+
+ {header.isPlaceholder ? null : ( + + )} +
+ {!header.isPlaceholder && header.column.getCanPin() && ( +
+ {header.column.getIsPinned() !== 'start' ? ( + + ) : null} + {header.column.getIsPinned() ? ( + + ) : null} + {header.column.getIsPinned() !== 'end' ? ( + + ) : null} +
+ )} +
+ +
+ + + {table.getEndHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table + .getRowModel() + .rows.slice(0, 20) + .map((row) => { + return ( + + {row.getEndVisibleCells().map((cell) => { + return ( + + ) + })} + + ) + })} + +
+
+ {header.isPlaceholder ? null : ( + + )} +
+ {!header.isPlaceholder && header.column.getCanPin() && ( +
+ {header.column.getIsPinned() !== 'start' ? ( + + ) : null} + {header.column.getIsPinned() ? ( + + ) : null} + {header.column.getIsPinned() !== 'end' ? ( + + ) : null} +
+ )} +
+ +
+
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/column-pinning-split/src/makeData.ts b/examples/octane/column-pinning-split/src/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/octane/column-pinning-split/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/column-pinning-split/src/vite-env.d.ts b/examples/octane/column-pinning-split/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/column-pinning-split/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/column-pinning-split/tests/e2e/smoke.spec.ts b/examples/octane/column-pinning-split/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..1b1be7072b --- /dev/null +++ b/examples/octane/column-pinning-split/tests/e2e/smoke.spec.ts @@ -0,0 +1,210 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +const LEAF_COUNT = 6 + +/** The three tables render the start pinned, unpinned and end pinned columns. */ +const START_TABLE = 0 +const CENTER_TABLE = 1 +const END_TABLE = 2 + +let server: Awaited> | undefined + +test.beforeAll(async () => { + // One server for the whole file. A cold Vite start costs more than a test. + test.setTimeout(180_000) + server = await startExampleServer(exampleDir) +}) + +test.afterAll(async () => { + await server?.close() +}) + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + if (!server) throw new Error('Example server failed to start') + + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return errors +} + +function tableAt(page: Page, index: number) { + return page.locator('table.outlined-table').nth(index) +} + +/** Header rows run group to leaf, so the leaf row holds the data columns. */ +function leafHeaderCells(page: Page, tableIndex: number) { + return tableAt(page, tableIndex).locator('thead tr').last().locator('th') +} + +function leafHeader(page: Page, tableIndex: number, name: string) { + return leafHeaderCells(page, tableIndex).filter({ hasText: name }) +} + +/** An unpinned header offers `<=` and `=>`; a pinned one swaps in `X`. */ +function pinButton( + page: Page, + tableIndex: number, + name: string, + label: '<=' | 'X' | '=>', +) { + return leafHeader(page, tableIndex, name).getByRole('button', { + name: label, + exact: true, + }) +} + +/** Row data is random faker output, so `table.state` is the stable oracle. */ +async function readColumnPinning(page: Page) { + const text = await page.getByTestId('table-state').textContent() + const state = JSON.parse(text ?? '{}') as { + columnPinning?: { start?: Array; end?: Array } + } + + return { + start: state.columnPinning?.start ?? [], + end: state.columnPinning?.end ?? [], + } +} + +async function leafCount(page: Page, tableIndex: number) { + return leafHeaderCells(page, tableIndex).count() +} + +async function getFirstBodyRowText(page: Page) { + const text = await tableAt(page, CENTER_TABLE) + .locator('tbody tr') + .first() + .textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const errors = await openExample(page) + + // One table per pinning region, even when two of them are empty. + await expect(page.locator('table.outlined-table')).toHaveCount(3) + expect(await readColumnPinning(page)).toEqual({ start: [], end: [] }) + // Nothing is pinned, so every column lives in the centre table. + expect(await leafCount(page, START_TABLE)).toBe(0) + expect(await leafCount(page, CENTER_TABLE)).toBe(LEAF_COUNT) + expect(await leafCount(page, END_TABLE)).toBe(0) + + expect(errors).toEqual([]) +}) + +test('regenerates table data', async ({ page }) => { + const errors = await openExample(page) + const regenerateButton = page.getByRole('button', { + name: /^Regenerate Data$/i, + }) + + await expect( + tableAt(page, CENTER_TABLE).locator('tbody tr').first(), + ).toBeVisible() + + const firstRowBefore = await getFirstBodyRowText(page) + + await regenerateButton.click() + + await expect.poll(() => getFirstBodyRowText(page)).not.toBe(firstRowBefore) + + expect(errors).toEqual([]) +}) + +test('moves a column into the start table when pinned', async ({ page }) => { + const errors = await openExample(page) + + await pinButton(page, CENTER_TABLE, 'Visits', '<=').click() + + await expect + .poll(async () => (await readColumnPinning(page)).start) + .toEqual(['visits']) + // The column leaves the centre table and appears in the start table, so the + // split is driven by pinning state rather than by a separate column list. + await expect.poll(() => leafCount(page, START_TABLE)).toBe(1) + expect(await leafCount(page, CENTER_TABLE)).toBe(LEAF_COUNT - 1) + await expect(leafHeaderCells(page, START_TABLE)).toContainText(['Visits']) + await expect(leafHeaderCells(page, CENTER_TABLE)).not.toContainText([ + 'Visits', + ]) + + expect(errors).toEqual([]) +}) + +test('moves a column into the end table when pinned', async ({ page }) => { + const errors = await openExample(page) + + await pinButton(page, CENTER_TABLE, 'Age', '=>').click() + + await expect + .poll(async () => (await readColumnPinning(page)).end) + .toEqual(['age']) + await expect.poll(() => leafCount(page, END_TABLE)).toBe(1) + expect(await leafCount(page, CENTER_TABLE)).toBe(LEAF_COUNT - 1) + await expect(leafHeaderCells(page, END_TABLE)).toContainText(['Age']) + + expect(errors).toEqual([]) +}) + +test('returns a column to the centre table when unpinned', async ({ page }) => { + const errors = await openExample(page) + + await pinButton(page, CENTER_TABLE, 'Visits', '<=').click() + await expect.poll(() => leafCount(page, START_TABLE)).toBe(1) + + await pinButton(page, START_TABLE, 'Visits', 'X').click() + + await expect + .poll(() => readColumnPinning(page)) + .toEqual({ start: [], end: [] }) + await expect.poll(() => leafCount(page, START_TABLE)).toBe(0) + expect(await leafCount(page, CENTER_TABLE)).toBe(LEAF_COUNT) + + expect(errors).toEqual([]) +}) + +test('keeps every table showing the same rows', async ({ page }) => { + const errors = await openExample(page) + + await pinButton(page, CENTER_TABLE, 'Visits', '<=').click() + await expect.poll(() => leafCount(page, START_TABLE)).toBe(1) + await pinButton(page, CENTER_TABLE, 'Age', '=>').click() + await expect.poll(() => leafCount(page, END_TABLE)).toBe(1) + + // The three tables are one table split three ways, so their bodies must stay + // row for row aligned or the split would visibly tear. + const rowCounts = await Promise.all( + [START_TABLE, CENTER_TABLE, END_TABLE].map((index) => + tableAt(page, index).locator('tbody tr').count(), + ), + ) + + expect(rowCounts[0]).toBeGreaterThan(0) + expect(rowCounts[1]).toBe(rowCounts[0]) + expect(rowCounts[2]).toBe(rowCounts[0]) + + expect(errors).toEqual([]) +}) diff --git a/examples/octane/column-pinning-split/tsconfig.json b/examples/octane/column-pinning-split/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/column-pinning-split/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/column-pinning-split/vite.config.ts b/examples/octane/column-pinning-split/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/column-pinning-split/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/column-pinning-sticky/index.html b/examples/octane/column-pinning-sticky/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/column-pinning-sticky/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/column-pinning-sticky/package.json b/examples/octane/column-pinning-sticky/package.json new file mode 100644 index 0000000000..ea6f98732b --- /dev/null +++ b/examples/octane/column-pinning-sticky/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-column-pinning-sticky", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/column-pinning-sticky/src/index.css b/examples/octane/column-pinning-sticky/src/index.css new file mode 100644 index 0000000000..dc0c562e94 --- /dev/null +++ b/examples/octane/column-pinning-sticky/src/index.css @@ -0,0 +1,396 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +.table-container { + border: 1px solid lightgray; + overflow-x: scroll; + width: 100%; + max-width: 960px; + position: relative; + border-collapse: collapse; + border-spacing: 0; +} + +table { + /* box-shadow and borders will not work with position: sticky otherwise */ + border-collapse: collapse; + border-spacing: 0; + table-layout: fixed; +} + +th { + background-color: lightgray; + border-bottom: 1px solid lightgray; + box-sizing: border-box; + font-weight: bold; + height: 30px; + overflow: hidden; + padding: 2px 4px; + position: relative; + text-align: center; +} + +td { + background-color: white; + box-sizing: border-box; + overflow: hidden; + padding: 2px 4px; +} + +.resizer { + background: rgba(0, 0, 0, 0.5); + cursor: col-resize; + height: 100%; + position: absolute; + right: 0; + top: 0; + touch-action: none; + user-select: none; + width: 5px; +} + +.resizer.isResizing { + background: blue; + opacity: 1; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/column-pinning-sticky/src/main.tsrx b/examples/octane/column-pinning-sticky/src/main.tsrx new file mode 100644 index 0000000000..e9eaef5923 --- /dev/null +++ b/examples/octane/column-pinning-sticky/src/main.tsrx @@ -0,0 +1,286 @@ +import { useState } from 'octane' +import { createRoot } from 'octane' +import { + columnOrderingFeature, + columnPinningFeature, + columnResizingFeature, + columnSizingFeature, + columnVisibilityFeature, + createColumnHelper, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { faker } from '@faker-js/faker' +import { makeData } from './makeData' +import type { Column } from '@tanstack/octane-table' +import type { JSX } from 'octane/jsx-runtime' +import type { Person } from './makeData' +import './index.css' + +type Style = Extract, object> + +const features = tableFeatures({ + columnOrderingFeature, + columnPinningFeature, + columnResizingFeature, + columnSizingFeature, + columnVisibilityFeature, +}) + +const columnHelper = createColumnHelper() +// These are the important styles to make sticky column pinning work! +// Apply styles like this using your CSS strategy of choice with this kind of logic to head cells, data cells, footer cells, etc. +// View the index.css file for more needed styles such as border-collapse: collapse +const getCommonPinningStyles = ( + column: Column, +): Style => { + const isPinned = column.getIsPinned() + const isLastLeftPinnedColumn = + isPinned === 'start' && column.getIsLastColumn('start') + const isFirstRightPinnedColumn = + isPinned === 'end' && column.getIsFirstColumn('end') + + return { + boxShadow: isLastLeftPinnedColumn + ? '-4px 0 4px -4px gray inset' + : isFirstRightPinnedColumn + ? '4px 0 4px -4px gray inset' + : undefined, + insetInlineStart: + isPinned === 'start' ? `${column.getStart('start')}px` : undefined, + insetInlineEnd: + isPinned === 'end' ? `${column.getAfter('end')}px` : undefined, + opacity: isPinned ? 0.95 : 1, + position: isPinned ? 'sticky' : 'relative', + width: column.getSize(), + zIndex: isPinned ? 1 : 0, + } +} + +const defaultColumns = columnHelper.columns([ + columnHelper.accessor('firstName', { + id: 'firstName', + header: 'First Name', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + size: 180, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + footer: (props) => props.column.id, + size: 180, + }), + columnHelper.accessor('age', { + id: 'age', + header: 'Age', + footer: (props) => props.column.id, + size: 180, + }), + columnHelper.accessor('visits', { + id: 'visits', + header: 'Visits', + footer: (props) => props.column.id, + size: 180, + }), + columnHelper.accessor('status', { + id: 'status', + header: 'Status', + footer: (props) => props.column.id, + size: 180, + }), + columnHelper.accessor('progress', { + id: 'progress', + header: 'Profile Progress', + footer: (props) => props.column.id, + size: 180, + }), +]) + +function App() @{ + const [data, setData] = useState(() => makeData(20)) + const [columns] = useState(() => [...defaultColumns]) + + const refreshData = () => setData(() => makeData(20)) + const stressTest = () => setData(() => makeData(1_000)) + + const table = useTable( + { + features, + columns, + data, + columnResizeMode: 'onChange', + // initialState: { columnPinning: { start: ['firstName'], end: [] } }, // `start`/`end` follow layout direction + // atoms: { columnPinning: columnPinningAtom }, // preferred: own pinning state with an external atom + // state: { columnPinning }, // classic controlled state; pair with onColumnPinningChange + // onColumnPinningChange: setColumnPinning, + // enableColumnPinning: false, // disable pinning for every column; default true + debugTable: true, + debugHeaders: true, + debugColumns: true, + }, + (state) => state, // default selector + ) + + const randomizeColumns = () => { + table.setColumnOrder( + faker.helpers.shuffle(table.getAllLeafColumns().map((d) => d.id)), + ) + } + +
+
+
+ +
+ {table.getAllLeafColumns().map((column) => { + return ( +
+ +
+ ) + })} +
+
+
+ + + +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const { column } = header + + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => { + const { column } = cell + return ( + + ) + })} + + ))} + +
+
+ {header.isPlaceholder ? null : ( + <> + {' '} + + )} + {/* Demo getIndex behavior */} + {column.getIndex(column.getIsPinned() || 'center')} +
+ {!header.isPlaceholder && header.column.getCanPin() && ( +
+ {header.column.getIsPinned() !== 'start' ? ( + + ) : null} + {header.column.getIsPinned() ? ( + + ) : null} + {header.column.getIsPinned() !== 'end' ? ( + + ) : null} +
+ )} +
header.column.resetSize()} + onMouseDown={header.getResizeHandler()} + onTouchStart={header.getResizeHandler()} + className={`resizer ${ + header.column.getIsResizing() ? 'isResizing' : '' + }`} + /> +
+ +
+
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/column-pinning-sticky/src/makeData.ts b/examples/octane/column-pinning-sticky/src/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/octane/column-pinning-sticky/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/column-pinning-sticky/src/vite-env.d.ts b/examples/octane/column-pinning-sticky/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/column-pinning-sticky/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/column-pinning-sticky/tests/e2e/smoke.spec.ts b/examples/octane/column-pinning-sticky/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..d5619d34ea --- /dev/null +++ b/examples/octane/column-pinning-sticky/tests/e2e/smoke.spec.ts @@ -0,0 +1,198 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +let server: Awaited> | undefined + +test.beforeAll(async () => { + // One server for the whole file. A cold Vite start costs more than a test. + test.setTimeout(180_000) + server = await startExampleServer(exampleDir) +}) + +test.afterAll(async () => { + await server?.close() +}) + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + if (!server) throw new Error('Example server failed to start') + + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return errors +} + +/** Header rows run group to leaf, so the leaf row holds the data columns. */ +function leafHeaderCells(page: Page) { + return page.locator('thead tr').last().locator('th') +} + +function leafHeader(page: Page, name: string) { + return leafHeaderCells(page).filter({ hasText: name }) +} + +/** An unpinned header offers `<=` and `=>`; a pinned one swaps in `X`. */ +function pinButton(page: Page, name: string, label: '<=' | 'X' | '=>') { + return leafHeader(page, name).getByRole('button', { + name: label, + exact: true, + }) +} + +function scrollContainer(page: Page) { + return page.locator('.table-container') +} + +/** Row data is random faker output, so `table.state` is the stable oracle. */ +async function readColumnPinning(page: Page) { + const text = await page.getByTestId('table-state').textContent() + const state = JSON.parse(text ?? '{}') as { + columnPinning?: { start?: Array; end?: Array } + } + + return { + start: state.columnPinning?.start ?? [], + end: state.columnPinning?.end ?? [], + } +} + +/** + * Scrolls the container as far right as it goes and returns the distance + * travelled. How much room there is depends on the viewport, so the tests work + * from the real value rather than assuming one. + */ +async function scrollToEnd(page: Page) { + const maxScroll = await scrollContainer(page).evaluate( + (element) => element.scrollWidth - element.clientWidth, + ) + + expect(maxScroll).toBeGreaterThan(50) + + await scrollContainer(page).evaluate((element) => { + element.scrollLeft = element.scrollWidth + element.dispatchEvent(new Event('scroll')) + }) + + await expect + .poll(() => scrollContainer(page).evaluate((element) => element.scrollLeft)) + .toBeGreaterThan(maxScroll - 2) + + return maxScroll +} + +async function getFirstBodyRowText(page: Page) { + const text = await page.locator('tbody tr').first().textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const errors = await openExample(page) + + await expect(page.locator('table').first()).toBeVisible() + await expect(page.locator('tbody tr')).toHaveCount(20) + await expect(scrollContainer(page)).toBeVisible() + expect(await readColumnPinning(page)).toEqual({ start: [], end: [] }) + // Unpinned columns scroll with the rest, so they are not sticky. + await expect(leafHeaderCells(page).first()).toHaveCSS('position', 'relative') + + expect(errors).toEqual([]) +}) + +test('regenerates table data', async ({ page }) => { + const errors = await openExample(page) + const regenerateButton = page.getByRole('button', { + name: /^Regenerate Data$/i, + }) + + await expect(page.locator('tbody tr').first()).toBeVisible() + + const firstRowBefore = await getFirstBodyRowText(page) + + await regenerateButton.click() + + await expect.poll(() => getFirstBodyRowText(page)).not.toBe(firstRowBefore) + await expect(page.locator('tbody tr')).toHaveCount(20) + + expect(errors).toEqual([]) +}) + +test('makes a pinned column sticky', async ({ page }) => { + const errors = await openExample(page) + + await pinButton(page, 'Visits', '<=').click() + + await expect + .poll(async () => (await readColumnPinning(page)).start) + .toEqual(['visits']) + // Pinning switches the cell from relative to sticky and gives it an offset, + // which is what actually holds it in place while the rest scrolls. + await expect(leafHeader(page, 'Visits')).toHaveCSS('position', 'sticky') + await expect(leafHeader(page, 'Visits')).toHaveCSS('left', '0px') + + expect(errors).toEqual([]) +}) + +test('holds a start pinned column in place while scrolling', async ({ + page, +}) => { + const errors = await openExample(page) + + await pinButton(page, 'Visits', '<=').click() + await expect(leafHeader(page, 'Visits')).toHaveCSS('position', 'sticky') + + const pinnedBefore = await leafHeader(page, 'Visits').boundingBox() + const unpinnedBefore = await leafHeader(page, 'Status').boundingBox() + + const scrolled = await scrollToEnd(page) + + const pinnedAfter = await leafHeader(page, 'Visits').boundingBox() + const unpinnedAfter = await leafHeader(page, 'Status').boundingBox() + + // The pinned header stays put while its unpinned neighbour travels left by + // the full scroll distance. That difference is the whole point of sticky. + expect(Math.abs((pinnedAfter?.x ?? 0) - (pinnedBefore?.x ?? 0))).toBeLessThan( + 2, + ) + expect((unpinnedBefore?.x ?? 0) - (unpinnedAfter?.x ?? 0)).toBeGreaterThan( + scrolled - 2, + ) + + expect(errors).toEqual([]) +}) + +test('unpins a column back into the scrolling area', async ({ page }) => { + const errors = await openExample(page) + + await pinButton(page, 'Visits', '<=').click() + await expect(leafHeader(page, 'Visits')).toHaveCSS('position', 'sticky') + + await pinButton(page, 'Visits', 'X').click() + + await expect + .poll(() => readColumnPinning(page)) + .toEqual({ start: [], end: [] }) + await expect(leafHeader(page, 'Visits')).toHaveCSS('position', 'relative') + + expect(errors).toEqual([]) +}) diff --git a/examples/octane/column-pinning-sticky/tsconfig.json b/examples/octane/column-pinning-sticky/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/column-pinning-sticky/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/column-pinning-sticky/vite.config.ts b/examples/octane/column-pinning-sticky/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/column-pinning-sticky/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/column-pinning/index.html b/examples/octane/column-pinning/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/column-pinning/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/column-pinning/package.json b/examples/octane/column-pinning/package.json new file mode 100644 index 0000000000..aad880d58b --- /dev/null +++ b/examples/octane/column-pinning/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-column-pinning", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/column-pinning/src/index.css b/examples/octane/column-pinning/src/index.css new file mode 100644 index 0000000000..efd0b26417 --- /dev/null +++ b/examples/octane/column-pinning/src/index.css @@ -0,0 +1,374 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td:last-child { + border-right: 0; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/column-pinning/src/main.tsrx b/examples/octane/column-pinning/src/main.tsrx new file mode 100644 index 0000000000..7d0e4495e4 --- /dev/null +++ b/examples/octane/column-pinning/src/main.tsrx @@ -0,0 +1,235 @@ +import { useState } from 'octane' +import { createRoot } from 'octane' +import { faker } from '@faker-js/faker' +import './index.css' +import { + columnOrderingFeature, + columnPinningFeature, + columnVisibilityFeature, + createTableHook, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Person } from './makeData' + +// Create table hook with features +const { useAppTable, createAppColumnHelper } = createTableHook({ + features: { + columnVisibilityFeature, + columnPinningFeature, + columnOrderingFeature, + }, + debugTable: true, + debugHeaders: true, + debugColumns: true, +}) + +// Create column helper +const columnHelper = createAppColumnHelper() + +// Define columns using columnHelper +const columns = columnHelper.columns([ + columnHelper.group({ + header: 'Name', + footer: (props) => props.column.id, + columns: columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + footer: (props) => props.column.id, + }), + ]), + }), + columnHelper.group({ + header: 'Info', + footer: (props) => props.column.id, + columns: columnHelper.columns([ + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + }), + columnHelper.group({ + header: 'More Info', + columns: columnHelper.columns([ + columnHelper.accessor('visits', { + header: () => Visits, + footer: (props) => props.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + }), + ]), + }), + ]), + }), +]) + +function App() @{ + const [data, setData] = useState(() => makeData(1_000)) + + const refreshData = () => setData(() => makeData(1_000)) + const stressTest = () => setData(() => makeData(1_000_000)) + + const table = useAppTable( + { + columns, + data, + // initialState: { columnPinning: { start: ['firstName'], end: [] } }, // `start`/`end` follow layout direction + // atoms: { columnPinning: columnPinningAtom }, // preferred: own pinning state with an external atom + // state: { columnPinning }, // classic controlled state; pair with onColumnPinningChange + // onColumnPinningChange: setColumnPinning, + // enableColumnPinning: false, // disable pinning for every column; default true + debugTable: true, + }, + (state) => state, // default selector + ) + + const randomizeColumns = () => { + table.setColumnOrder( + faker.helpers.shuffle(table.getAllLeafColumns().map((d) => d.id)), + ) + } + +
+
+
+ +
+ {table.getAllLeafColumns().map((column) => { + return ( +
+ +
+ ) + })} +
+
+
+ + + +
+
+

+ This example using the non-split APIs. Columns are just reordered within + 1 table instead of being split into 3 different tables. +

+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table + .getRowModel() + .rows.slice(0, 20) + .map((row) => { + return ( + + {row.getVisibleCells().map((cell) => { + return ( + + ) + })} + + ) + })} + +
+
+ {header.isPlaceholder ? null : ( + + )} +
+ {!header.isPlaceholder && header.column.getCanPin() && ( +
+ {header.column.getIsPinned() !== 'start' ? ( + + ) : null} + {header.column.getIsPinned() ? ( + + ) : null} + {header.column.getIsPinned() !== 'end' ? ( + + ) : null} +
+ )} +
+ +
+
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/column-pinning/src/makeData.ts b/examples/octane/column-pinning/src/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/octane/column-pinning/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/column-pinning/src/vite-env.d.ts b/examples/octane/column-pinning/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/column-pinning/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/column-pinning/tests/e2e/smoke.spec.ts b/examples/octane/column-pinning/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..19dd97835c --- /dev/null +++ b/examples/octane/column-pinning/tests/e2e/smoke.spec.ts @@ -0,0 +1,228 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +const LEAF_HEADERS = [ + 'firstName', + 'Last Name', + 'Age', + 'Visits', + 'Status', + 'Profile Progress', +] + +let server: Awaited> | undefined + +test.beforeAll(async () => { + // One server for the whole file. A cold Vite start costs more than a test. + test.setTimeout(180_000) + server = await startExampleServer(exampleDir) +}) + +test.afterAll(async () => { + await server?.close() +}) + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + if (!server) throw new Error('Example server failed to start') + + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return errors +} + +/** Header rows run group to leaf, so the leaf row holds the data columns. */ +function leafHeaderCells(page: Page) { + return page.locator('thead tr').last().locator('th') +} + +function leafHeader(page: Page, name: string) { + return leafHeaderCells(page).filter({ hasText: name }) +} + +/** An unpinned header offers `<=` and `=>`; a pinned one swaps in `X`. */ +function pinButton(page: Page, name: string, label: '<=' | 'X' | '=>') { + return leafHeader(page, name).getByRole('button', { + name: label, + exact: true, + }) +} + +/** Row data is random faker output, so `table.state` is the stable oracle. */ +async function readColumnPinning(page: Page) { + const text = await page.getByTestId('table-state').textContent() + const state = JSON.parse(text ?? '{}') as { + columnPinning?: { start?: Array; end?: Array } + } + + return { + start: state.columnPinning?.start ?? [], + end: state.columnPinning?.end ?? [], + } +} + +async function readLeafOrder(page: Page) { + const texts = await leafHeaderCells(page).allTextContents() + // Strip the pin controls that share the header cell with its label. + return texts.map((text) => text.replace(/<=|=>|X/g, '').trim()) +} + +async function getFirstBodyRowText(page: Page) { + const text = await page.locator('tbody tr').first().textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const errors = await openExample(page) + const table = page.locator('table').first() + + await expect(table).toBeVisible() + await expect(leafHeaderCells(page)).toHaveCount(LEAF_HEADERS.length) + expect(await readLeafOrder(page)).toEqual(LEAF_HEADERS) + expect(await readColumnPinning(page)).toEqual({ start: [], end: [] }) + // Nothing is pinned, so every header offers both directions and no unpin. + await expect(pinButton(page, 'Visits', '<=')).toBeVisible() + await expect(pinButton(page, 'Visits', '=>')).toBeVisible() + await expect(pinButton(page, 'Visits', 'X')).toHaveCount(0) + + expect(errors).toEqual([]) +}) + +test('regenerates table data', async ({ page }) => { + const errors = await openExample(page) + const regenerateButton = page.getByRole('button', { + name: /^Regenerate Data$/i, + }) + + await expect(page.locator('tbody tr').first()).toBeVisible() + + const firstRowBefore = await getFirstBodyRowText(page) + + await regenerateButton.click() + + await expect.poll(() => getFirstBodyRowText(page)).not.toBe(firstRowBefore) + + expect(errors).toEqual([]) +}) + +test('pins a column to the start', async ({ page }) => { + const errors = await openExample(page) + + await pinButton(page, 'Visits', '<=').click() + + await expect + .poll(async () => (await readColumnPinning(page)).start) + .toEqual(['visits']) + // The pinned column jumps to the front, keeping the others in order. + expect(await readLeafOrder(page)).toEqual([ + 'Visits', + 'firstName', + 'Last Name', + 'Age', + 'Status', + 'Profile Progress', + ]) + // A pinned header drops the direction it already occupies and gains unpin. + await expect(pinButton(page, 'Visits', '<=')).toHaveCount(0) + await expect(pinButton(page, 'Visits', 'X')).toBeVisible() + await expect(pinButton(page, 'Visits', '=>')).toBeVisible() + + expect(errors).toEqual([]) +}) + +test('pins a column to the end', async ({ page }) => { + const errors = await openExample(page) + + await pinButton(page, 'Age', '=>').click() + + await expect + .poll(async () => (await readColumnPinning(page)).end) + .toEqual(['age']) + expect(await readLeafOrder(page)).toEqual([ + 'firstName', + 'Last Name', + 'Visits', + 'Status', + 'Profile Progress', + 'Age', + ]) + await expect(pinButton(page, 'Age', '=>')).toHaveCount(0) + await expect(pinButton(page, 'Age', 'X')).toBeVisible() + + expect(errors).toEqual([]) +}) + +test('unpins a column', async ({ page }) => { + const errors = await openExample(page) + + await pinButton(page, 'Visits', '<=').click() + await expect + .poll(async () => (await readColumnPinning(page)).start) + .toEqual(['visits']) + + await pinButton(page, 'Visits', 'X').click() + + await expect + .poll(() => readColumnPinning(page)) + .toEqual({ start: [], end: [] }) + // Unpinning restores the column to its declared position. + expect(await readLeafOrder(page)).toEqual(LEAF_HEADERS) + await expect(pinButton(page, 'Visits', 'X')).toHaveCount(0) + + expect(errors).toEqual([]) +}) + +test('pins several columns in the order they were clicked', async ({ + page, +}) => { + const errors = await openExample(page) + + await pinButton(page, 'Visits', '<=').click() + await expect + .poll(async () => (await readColumnPinning(page)).start) + .toEqual(['visits']) + + await pinButton(page, 'Status', '<=').click() + + await expect + .poll(async () => (await readColumnPinning(page)).start) + .toEqual(['visits', 'status']) + + await pinButton(page, 'Age', '=>').click() + + await expect + .poll(async () => (await readColumnPinning(page)).end) + .toEqual(['age']) + // Start pins lead, unpinned columns hold the middle, end pins trail. + expect(await readLeafOrder(page)).toEqual([ + 'Visits', + 'Status', + 'firstName', + 'Last Name', + 'Profile Progress', + 'Age', + ]) + + expect(errors).toEqual([]) +}) diff --git a/examples/octane/column-pinning/tsconfig.json b/examples/octane/column-pinning/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/column-pinning/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/column-pinning/vite.config.ts b/examples/octane/column-pinning/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/column-pinning/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/column-resizing-performant/index.html b/examples/octane/column-resizing-performant/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/column-resizing-performant/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/column-resizing-performant/package.json b/examples/octane/column-resizing-performant/package.json new file mode 100644 index 0000000000..eeba8dc619 --- /dev/null +++ b/examples/octane/column-resizing-performant/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-column-resizing-performant", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/column-resizing-performant/src/index.css b/examples/octane/column-resizing-performant/src/index.css new file mode 100644 index 0000000000..649e092ce8 --- /dev/null +++ b/examples/octane/column-resizing-performant/src/index.css @@ -0,0 +1,400 @@ +* { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tr { + height: 30px; +} + +th, +td { + box-shadow: inset 0 0 0 1px lightgray; + padding: 0.25rem; +} + +th { + padding: 2px 4px; + position: relative; + font-weight: bold; + text-align: center; + height: 30px; +} + +td { + height: 30px; +} + +.resizer { + position: absolute; + top: 0; + height: 100%; + right: 0; + width: 5px; + background: rgba(0, 0, 0, 0.5); + cursor: col-resize; + user-select: none; + touch-action: none; +} + +.resizer.isResizing { + background: blue; + opacity: 1; +} + +@media (hover: hover) { + .resizer { + opacity: 0; + } + + *:hover > .resizer { + opacity: 1; + } +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/column-resizing-performant/src/main.tsrx b/examples/octane/column-resizing-performant/src/main.tsrx new file mode 100644 index 0000000000..d128e49ad2 --- /dev/null +++ b/examples/octane/column-resizing-performant/src/main.tsrx @@ -0,0 +1,238 @@ +import { useLayoutEffect, useRef, useState } from 'octane' +import { createRoot } from 'octane' +import { + columnResizingFeature, + columnSizingFeature, + createColumnHelper, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Table } from '@tanstack/octane-table' +import type { Person } from './makeData' +import './index.css' + +/** + * This example implements column resizing with NO Octane re-renders! + * Instead, we subscribe to the table store OUTSIDE of Octane and write CSS variables + */ + +const features = tableFeatures({ columnResizingFeature, columnSizingFeature }) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.group({ + header: 'Name', + footer: (props) => props.column.id, + columns: columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + footer: (props) => props.column.id, + }), + ]), + }), + columnHelper.group({ + header: 'Info', + footer: (props) => props.column.id, + columns: columnHelper.columns([ + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + }), + columnHelper.accessor('visits', { + header: () => Visits, + footer: (props) => props.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + }), + ]), + }), +]) + +function App() @{ + const [data, setData] = useState(() => makeData(200)) + const refreshData = () => setData(makeData(200)) + const stressTest = () => setData(makeData(5_000)) + + const table = useTable( + { + features, + columns, + data, + defaultColumn: { + minSize: 60, + maxSize: 800, + }, + columnResizeMode: 'onChange', + // initialState: { columnSizing: { firstName: 200 } }, // set column sizes on first render + // atoms: { columnResizing: columnResizingAtom }, // preferred: own transient resize state with an external atom + // state: { columnResizing }, // classic controlled state; pair with onColumnResizingChange + // onColumnResizingChange: setColumnResizing, + // columnResizeDirection: 'rtl', // calculate resize offsets right-to-left; default 'ltr' + // enableColumnResizing: false, // disable resizing for every column; default true + debugTable: true, + debugHeaders: true, + debugColumns: true, + }, + () => ({}), // subscribe to nothing instead of re-rendering on every internal state change + ) + + const tableRef = useRef(null) + + /** + * Instead of re-rendering Octane on every resize tick, we subscribe to the + * table store OUTSIDE of Octane and write the column size CSS variables + * directly onto the element. Header and data cells reference the + * variables, so the browser updates widths with zero Octane work per tick. + * (The core resize handler already coalesces pointer events to one state + * update per animation frame.) + */ + useLayoutEffect(() => { + const writeColumnSizeVars = () => { + const tableEl = tableRef.current + if (!tableEl) return + for (const header of table.getFlatHeaders()) { + tableEl.style.setProperty( + `--header-${header.id}-size`, + String(header.getSize()), + ) + tableEl.style.setProperty( + `--col-${header.column.id}-size`, + String(header.column.getSize()), + ) + } + tableEl.style.width = `${table.getTotalSize()}px` + } + writeColumnSizeVars() // initial paint + const { unsubscribe } = + table.atoms.columnSizing.subscribe(writeColumnSizeVars) + return () => unsubscribe() + }, [table.store]) + +
+
+ + +
+
+ {/* Only this little island re-renders per resize tick */} + state}> + {(state) => ( +
+            {JSON.stringify(state, null, 2)}
+          
+ )} +
+
({data.length.toLocaleString()} rows) +
+ {/* This example is using semantic table tags, but also CSS Grid/Flexbox layout for more absolute column widths */} +
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + {/* No memoization needed: the root subscribes to no table state, + so the body only re-renders when `data` itself changes */} + +
+ {header.isPlaceholder ? null : ( + + )} + {/* Each resizer subscribes to just its own "am I being + resized?" boolean, so a drag re-renders exactly one + of these islands at drag start and end */} + + state.columnResizing.isResizingColumn === + header.column.id + } + > + {(isResizing) => ( +
header.column.resetSize()} + onMouseDown={header.getResizeHandler()} + onTouchStart={header.getResizeHandler()} + className={`resizer ${isResizing ? 'isResizing' : ''}`} + /> + )} + +
+
+
+} + +interface TableBodyProps { + table: Table +} + +function TableBody({ table }: TableBodyProps) @{ + + {table.getRowModel().rows.map((row) => ( + + {row.getAllCells().map((cell) => { + return ( + + {cell.renderValue()} + + ) + })} + + ))} + +} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/column-resizing-performant/src/makeData.ts b/examples/octane/column-resizing-performant/src/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/octane/column-resizing-performant/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/column-resizing-performant/src/vite-env.d.ts b/examples/octane/column-resizing-performant/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/column-resizing-performant/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/column-resizing-performant/tests/e2e/smoke.spec.ts b/examples/octane/column-resizing-performant/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..868e818817 --- /dev/null +++ b/examples/octane/column-resizing-performant/tests/e2e/smoke.spec.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' +import path from 'node:path' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return { errors, server } +} + +function getTable(page: Page) { + return page + .locator('table:visible, .divTable:visible') + .filter({ has: page.locator('thead th, .thead .th') }) + .filter({ has: page.locator('tbody tr, .tbody .tr') }) + .first() +} + +function getHeaderCells(table: Locator) { + return table.locator('thead th, .thead .th') +} + +function getBodyRows(table: Locator) { + return table.locator('tbody tr, .tbody .tr') +} + +async function getFirstBodyRowData(table: Locator) { + const row = getBodyRows(table).first() + const firstInput = row + .locator('input:not([type="checkbox"]):not([type="radio"])') + .first() + + if ((await firstInput.count()) > 0 && (await firstInput.isVisible())) { + return firstInput.inputValue() + } + + const text = await row.textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const { errors, server } = await openExample(page) + + try { + const table = getTable(page) + const bodyRows = getBodyRows(table) + + await expect(table).toBeVisible() + await expect(getHeaderCells(table).first()).toBeVisible() + await expect(bodyRows.first()).toBeVisible() + + const regenerateButton = page + .getByRole('button', { name: /^Regenerate Data$/i }) + .first() + + if ((await regenerateButton.count()) > 0) { + await expect(regenerateButton).toBeVisible() + + const firstRowBefore = await getFirstBodyRowData(table) + + await regenerateButton.click() + + await expect + .poll(() => getFirstBodyRowData(table)) + .not.toBe(firstRowBefore) + await expect(bodyRows.first()).toBeVisible() + } + + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/column-resizing-performant/tsconfig.json b/examples/octane/column-resizing-performant/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/column-resizing-performant/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/column-resizing-performant/vite.config.ts b/examples/octane/column-resizing-performant/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/column-resizing-performant/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/column-resizing/index.html b/examples/octane/column-resizing/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/column-resizing/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/column-resizing/package.json b/examples/octane/column-resizing/package.json new file mode 100644 index 0000000000..f110f33fa5 --- /dev/null +++ b/examples/octane/column-resizing/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-column-resizing", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/column-resizing/src/index.css b/examples/octane/column-resizing/src/index.css new file mode 100644 index 0000000000..d7f648b224 --- /dev/null +++ b/examples/octane/column-resizing/src/index.css @@ -0,0 +1,419 @@ +* { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +table, +.divTable { + border: 1px solid lightgray; + width: fit-content; + border-collapse: collapse; + border-spacing: 0; +} + +.tr { + display: flex; +} + +tr, +.tr { + width: fit-content; + height: 30px; +} + +th, +.th, +td, +.td { + box-shadow: inset 0 0 0 1px lightgray; + padding: 0.25rem; +} + +th, +.th { + padding: 2px 4px; + position: relative; + font-weight: bold; + text-align: center; + height: 30px; +} + +td, +.td { + height: 30px; +} + +.resizer { + position: absolute; + top: 0; + height: 100%; + width: 5px; + background: rgba(0, 0, 0, 0.5); + cursor: col-resize; + user-select: none; + touch-action: none; +} + +.resizer.ltr { + right: 0; +} + +.resizer.rtl { + left: 0; +} + +.resizer.isResizing { + background: blue; + opacity: 1; +} + +@media (hover: hover) { + .resizer { + opacity: 0; + } + + *:hover > .resizer { + opacity: 1; + } +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/column-resizing/src/main.tsrx b/examples/octane/column-resizing/src/main.tsrx new file mode 100644 index 0000000000..1454913daa --- /dev/null +++ b/examples/octane/column-resizing/src/main.tsrx @@ -0,0 +1,345 @@ +import { useState } from 'octane' +import { createRoot } from 'octane' +import { + columnResizingFeature, + columnSizingFeature, + createColumnHelper, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Person } from './makeData' +import type { + ColumnResizeDirection, + ColumnResizeMode, +} from '@tanstack/octane-table' +import './index.css' + +const features = tableFeatures({ columnResizingFeature, columnSizingFeature }) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.group({ + header: 'Name', + footer: (props) => props.column.id, + columns: columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + footer: (props) => props.column.id, + }), + ]), + }), + columnHelper.group({ + header: 'Info', + footer: (props) => props.column.id, + columns: columnHelper.columns([ + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + }), + columnHelper.group({ + header: 'More Info', + columns: columnHelper.columns([ + columnHelper.accessor('visits', { + header: () => Visits, + footer: (props) => props.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + }), + ]), + }), + ]), + }), +]) + +function App() @{ + const [data, setData] = useState(() => makeData(10)) + const refreshData = () => setData(makeData(10)) + const stressTest = () => setData(makeData(100)) + + const [columnResizeMode, setColumnResizeMode] = + useState('onChange') + + const [columnResizeDirection, setColumnResizeDirection] = + useState('ltr') + + const table = useTable( + { + features, + columns, + data, + columnResizeMode, + columnResizeDirection, + // initialState: { columnSizing: { firstName: 200 } }, // set column sizes on first render + // atoms: { columnResizing: columnResizingAtom }, // preferred: own transient resize state with an external atom + // state: { columnResizing }, // classic controlled state; pair with onColumnResizingChange + // onColumnResizingChange: setColumnResizing, + // enableColumnResizing: false, // disable resizing for every column; default true + debugTable: true, + debugHeaders: true, + debugColumns: true, + }, + (state) => state, // default selector + ) + +
+
+ + +
+ + +
+
+
{''} +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getAllCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( + + )} +
header.column.resetSize()} + onMouseDown={header.getResizeHandler()} + onTouchStart={header.getResizeHandler()} + className={`resizer ${ + table.options.columnResizeDirection + } ${header.column.getIsResizing() ? 'isResizing' : ''}`} + style={{ + transform: + columnResizeMode === 'onEnd' && + header.column.getIsResizing() + ? `translateX(${ + (table.options.columnResizeDirection === 'rtl' + ? -1 + : 1) * + (table.state.columnResizing.deltaOffset ?? 0) + }px)` + : '', + }} + /> +
+ +
+
+
+
{'
(relative)'}
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => ( +
+ {headerGroup.headers.map((header) => ( +
+ {header.isPlaceholder ? null : ( + + )} +
header.column.resetSize()} + onMouseDown={header.getResizeHandler()} + onTouchStart={header.getResizeHandler()} + className={`resizer ${ + table.options.columnResizeDirection + } ${header.column.getIsResizing() ? 'isResizing' : ''}`} + style={{ + transform: + columnResizeMode === 'onEnd' && + header.column.getIsResizing() + ? `translateX(${ + (table.options.columnResizeDirection === 'rtl' + ? -1 + : 1) * + (table.state.columnResizing.deltaOffset ?? 0) + }px)` + : '', + }} + /> +
+ ))} +
+ ))} +
+
+ {table.getRowModel().rows.map((row) => ( +
+ {row.getAllCells().map((cell) => ( +
+ +
+ ))} +
+ ))} +
+
+
+
+
{'
(absolute positioning)'}
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => ( +
+ {headerGroup.headers.map((header) => ( +
+ {header.isPlaceholder ? null : ( + + )} +
header.column.resetSize()} + onMouseDown={header.getResizeHandler()} + onTouchStart={header.getResizeHandler()} + className={`resizer ${ + table.options.columnResizeDirection + } ${header.column.getIsResizing() ? 'isResizing' : ''}`} + style={{ + transform: + columnResizeMode === 'onEnd' && + header.column.getIsResizing() + ? `translateX(${ + (table.options.columnResizeDirection === 'rtl' + ? -1 + : 1) * + (table.state.columnResizing.deltaOffset ?? 0) + }px)` + : '', + }} + /> +
+ ))} +
+ ))} +
+
+ {table.getRowModel().rows.map((row) => ( +
+ {row.getAllCells().map((cell) => ( +
+ +
+ ))} +
+ ))} +
+
+
+
+
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/column-resizing/src/makeData.ts b/examples/octane/column-resizing/src/makeData.ts new file mode 100644 index 0000000000..c0f87b8b19 --- /dev/null +++ b/examples/octane/column-resizing/src/makeData.ts @@ -0,0 +1,46 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + status: string + progress: number +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/column-resizing/src/vite-env.d.ts b/examples/octane/column-resizing/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/column-resizing/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/column-resizing/tests/e2e/smoke.spec.ts b/examples/octane/column-resizing/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..868e818817 --- /dev/null +++ b/examples/octane/column-resizing/tests/e2e/smoke.spec.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' +import path from 'node:path' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return { errors, server } +} + +function getTable(page: Page) { + return page + .locator('table:visible, .divTable:visible') + .filter({ has: page.locator('thead th, .thead .th') }) + .filter({ has: page.locator('tbody tr, .tbody .tr') }) + .first() +} + +function getHeaderCells(table: Locator) { + return table.locator('thead th, .thead .th') +} + +function getBodyRows(table: Locator) { + return table.locator('tbody tr, .tbody .tr') +} + +async function getFirstBodyRowData(table: Locator) { + const row = getBodyRows(table).first() + const firstInput = row + .locator('input:not([type="checkbox"]):not([type="radio"])') + .first() + + if ((await firstInput.count()) > 0 && (await firstInput.isVisible())) { + return firstInput.inputValue() + } + + const text = await row.textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const { errors, server } = await openExample(page) + + try { + const table = getTable(page) + const bodyRows = getBodyRows(table) + + await expect(table).toBeVisible() + await expect(getHeaderCells(table).first()).toBeVisible() + await expect(bodyRows.first()).toBeVisible() + + const regenerateButton = page + .getByRole('button', { name: /^Regenerate Data$/i }) + .first() + + if ((await regenerateButton.count()) > 0) { + await expect(regenerateButton).toBeVisible() + + const firstRowBefore = await getFirstBodyRowData(table) + + await regenerateButton.click() + + await expect + .poll(() => getFirstBodyRowData(table)) + .not.toBe(firstRowBefore) + await expect(bodyRows.first()).toBeVisible() + } + + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/column-resizing/tsconfig.json b/examples/octane/column-resizing/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/column-resizing/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/column-resizing/vite.config.ts b/examples/octane/column-resizing/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/column-resizing/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/column-sizing/index.html b/examples/octane/column-sizing/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/column-sizing/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/column-sizing/package.json b/examples/octane/column-sizing/package.json new file mode 100644 index 0000000000..01e52137fa --- /dev/null +++ b/examples/octane/column-sizing/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-column-sizing", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/column-sizing/src/index.css b/examples/octane/column-sizing/src/index.css new file mode 100644 index 0000000000..d7f648b224 --- /dev/null +++ b/examples/octane/column-sizing/src/index.css @@ -0,0 +1,419 @@ +* { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +table, +.divTable { + border: 1px solid lightgray; + width: fit-content; + border-collapse: collapse; + border-spacing: 0; +} + +.tr { + display: flex; +} + +tr, +.tr { + width: fit-content; + height: 30px; +} + +th, +.th, +td, +.td { + box-shadow: inset 0 0 0 1px lightgray; + padding: 0.25rem; +} + +th, +.th { + padding: 2px 4px; + position: relative; + font-weight: bold; + text-align: center; + height: 30px; +} + +td, +.td { + height: 30px; +} + +.resizer { + position: absolute; + top: 0; + height: 100%; + width: 5px; + background: rgba(0, 0, 0, 0.5); + cursor: col-resize; + user-select: none; + touch-action: none; +} + +.resizer.ltr { + right: 0; +} + +.resizer.rtl { + left: 0; +} + +.resizer.isResizing { + background: blue; + opacity: 1; +} + +@media (hover: hover) { + .resizer { + opacity: 0; + } + + *:hover > .resizer { + opacity: 1; + } +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/column-sizing/src/main.tsrx b/examples/octane/column-sizing/src/main.tsrx new file mode 100644 index 0000000000..3fe9336714 --- /dev/null +++ b/examples/octane/column-sizing/src/main.tsrx @@ -0,0 +1,276 @@ +import { useMemo, useState } from 'octane' +import { createRoot } from 'octane' +import { + columnSizingFeature, + createColumnHelper, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Person } from './makeData' +import './index.css' + +const features = tableFeatures({ columnSizingFeature }) + +const columnHelper = createColumnHelper() + +// This is not the Column Resizing Example, this is a simplified version that just sets static column sizes +function App() @{ + const [data, setData] = useState(() => makeData(20)) + const refreshData = () => setData(makeData(20)) + const stressTest = () => setData(makeData(1_000)) + + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + size: 120, // initial size + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + footer: (props) => props.column.id, + size: 120, + }), + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + size: 100, + }), + columnHelper.accessor('visits', { + header: () => Visits, + footer: (props) => props.column.id, + size: 80, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + size: 200, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + size: 200, + }), + ]), + [], + ) + + const table = useTable( + { + features, + columns, + data, + // initialState: { columnSizing: { firstName: 200 } }, // set column sizes on first render + // atoms: { columnSizing: columnSizingAtom }, // preferred: own sizing state with an external atom + // state: { columnSizing }, // classic controlled state; pair with onColumnSizingChange + // onColumnSizingChange: setColumnSizing, + debugTable: true, + debugHeaders: true, + debugColumns: true, + }, + (state) => state, // default selector + ) + +
+
+ + +
+
+
{'Initial Column Sizes'}
+
+ {table.getAllColumns().map((column) => ( +
+ +
+ ))} +
+
+
+
{''} +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getAllCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( + + )} +
+
+ +
+
+
+
{'
(relative)'}
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => ( +
+ {headerGroup.headers.map((header) => ( +
+ {header.isPlaceholder ? null : ( + + )} +
+
+ ))} +
+ ))} +
+
+ {table.getRowModel().rows.map((row) => ( +
+ {row.getAllCells().map((cell) => ( +
+ +
+ ))} +
+ ))} +
+
+
+
+
{'
(absolute positioning)'}
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => ( +
+ {headerGroup.headers.map((header) => ( +
+ {header.isPlaceholder ? null : ( + + )} +
+
+ ))} +
+ ))} +
+
+ {table.getRowModel().rows.map((row) => ( +
+ {row.getAllCells().map((cell) => ( +
+ +
+ ))} +
+ ))} +
+
+
+
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/column-sizing/src/makeData.ts b/examples/octane/column-sizing/src/makeData.ts new file mode 100644 index 0000000000..c0f87b8b19 --- /dev/null +++ b/examples/octane/column-sizing/src/makeData.ts @@ -0,0 +1,46 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + status: string + progress: number +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/column-sizing/src/vite-env.d.ts b/examples/octane/column-sizing/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/column-sizing/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/column-sizing/tests/e2e/smoke.spec.ts b/examples/octane/column-sizing/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..6dc5bd0bd3 --- /dev/null +++ b/examples/octane/column-sizing/tests/e2e/smoke.spec.ts @@ -0,0 +1,191 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +/** Declared `size` per column, in render order. */ +const INITIAL_SIZES: Record = { + firstName: 120, + lastName: 120, + age: 100, + visits: 80, + status: 200, + progress: 200, +} + +const LEAF_COLUMNS = Object.keys(INITIAL_SIZES) + +let server: Awaited> | undefined + +test.beforeAll(async () => { + // One server for the whole file. A cold Vite start costs more than a test. + test.setTimeout(180_000) + server = await startExampleServer(exampleDir) +}) + +test.afterAll(async () => { + await server?.close() +}) + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + if (!server) throw new Error('Example server failed to start') + + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return errors +} + +/** One number input per column in the Initial Column Sizes panel. */ +function sizeInput(page: Page, column: string) { + return page + .locator('label') + .filter({ hasText: new RegExp(`^${column}$`) }) + .locator('input.column-size-input') +} + +/** The example renders the same table three ways to prove they stay in sync. */ +function nativeHeader(page: Page, index: number) { + return page.locator('table thead th').nth(index) +} + +function relativeDivHeader(page: Page, index: number) { + return page.locator('.divTable').first().locator('.th').nth(index) +} + +function absoluteDivHeader(page: Page, index: number) { + return page.locator('.divTable').nth(1).locator('.th').nth(index) +} + +/** Row data is random faker output, so `table.state` is the stable oracle. */ +async function readColumnSizing(page: Page) { + const text = await page.getByTestId('table-state').textContent() + const state = JSON.parse(text ?? '{}') as { + columnSizing?: Record + } + + return state.columnSizing ?? {} +} + +async function widthOf(page: Page, index: number) { + const box = await nativeHeader(page, index).boundingBox() + return box?.width ?? 0 +} + +async function getFirstBodyRowText(page: Page) { + const text = await page.locator('tbody tr').first().textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const errors = await openExample(page) + + await expect(page.locator('table').first()).toBeVisible() + await expect(page.locator('tbody tr')).toHaveCount(20) + // No overrides applied yet, so sizes come straight from the column defs. + expect(await readColumnSizing(page)).toEqual({}) + + expect(errors).toEqual([]) +}) + +test('regenerates table data', async ({ page }) => { + const errors = await openExample(page) + const regenerateButton = page.getByRole('button', { + name: /^Regenerate Data$/i, + }) + + await expect(page.locator('tbody tr').first()).toBeVisible() + + const firstRowBefore = await getFirstBodyRowText(page) + + await regenerateButton.click() + + await expect.poll(() => getFirstBodyRowText(page)).not.toBe(firstRowBefore) + await expect(page.locator('tbody tr')).toHaveCount(20) + + expect(errors).toEqual([]) +}) + +test('renders each column at its declared size', async ({ page }) => { + const errors = await openExample(page) + + for (const [index, column] of LEAF_COLUMNS.entries()) { + const expected = INITIAL_SIZES[column] ?? 0 + await expect(sizeInput(page, column)).toHaveValue(String(expected)) + // Layout rounds, so allow a pixel of slack rather than an exact match. + expect(Math.abs((await widthOf(page, index)) - expected)).toBeLessThan(2) + } + + expect(errors).toEqual([]) +}) + +test('resizes a column from the size panel', async ({ page }) => { + const errors = await openExample(page) + const before = await widthOf(page, 0) + + await sizeInput(page, 'firstName').fill('300') + + await expect.poll(() => readColumnSizing(page)).toEqual({ firstName: 300 }) + await expect.poll(() => widthOf(page, 0)).toBeGreaterThan(before + 100) + expect(Math.abs((await widthOf(page, 0)) - 300)).toBeLessThan(2) + + // Its neighbours keep their declared widths. + expect(Math.abs((await widthOf(page, 2)) - 100)).toBeLessThan(2) + + expect(errors).toEqual([]) +}) + +test('keeps all three table renderings the same width', async ({ page }) => { + const errors = await openExample(page) + + await sizeInput(page, 'status').fill('260') + + await expect.poll(() => readColumnSizing(page)).toEqual({ status: 260 }) + + // The native table, the relative div table and the absolutely positioned div + // table all read the same column size, so a change has to land in all three. + const native = await nativeHeader(page, 4).boundingBox() + const relative = await relativeDivHeader(page, 4).boundingBox() + const absolute = await absoluteDivHeader(page, 4).boundingBox() + + expect(Math.abs((native?.width ?? 0) - 260)).toBeLessThan(2) + expect(Math.abs((relative?.width ?? 0) - 260)).toBeLessThan(2) + expect(Math.abs((absolute?.width ?? 0) - 260)).toBeLessThan(2) + + expect(errors).toEqual([]) +}) + +test('keeps column sizes when data is regenerated', async ({ page }) => { + const errors = await openExample(page) + + await sizeInput(page, 'age').fill('180') + await expect.poll(() => readColumnSizing(page)).toEqual({ age: 180 }) + + // Sizing lives in table state, so replacing the rows must not reset it. + await page.getByRole('button', { name: /^Regenerate Data$/i }).click() + + await expect(page.locator('tbody tr')).toHaveCount(20) + expect(await readColumnSizing(page)).toEqual({ age: 180 }) + expect(Math.abs((await widthOf(page, 2)) - 180)).toBeLessThan(2) + + expect(errors).toEqual([]) +}) diff --git a/examples/octane/column-sizing/tsconfig.json b/examples/octane/column-sizing/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/column-sizing/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/column-sizing/vite.config.ts b/examples/octane/column-sizing/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/column-sizing/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/column-visibility/index.html b/examples/octane/column-visibility/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/column-visibility/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/column-visibility/package.json b/examples/octane/column-visibility/package.json new file mode 100644 index 0000000000..9f87a767ca --- /dev/null +++ b/examples/octane/column-visibility/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-column-visibility", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/column-visibility/src/index.css b/examples/octane/column-visibility/src/index.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/octane/column-visibility/src/index.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/column-visibility/src/main.tsrx b/examples/octane/column-visibility/src/main.tsrx new file mode 100644 index 0000000000..d235c4d260 --- /dev/null +++ b/examples/octane/column-visibility/src/main.tsrx @@ -0,0 +1,168 @@ +import { useState } from 'octane' +import { createRoot } from 'octane' +import { + columnVisibilityFeature, + createColumnHelper, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Person } from './makeData' +import './index.css' + +const features = tableFeatures({ columnVisibilityFeature }) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.group({ + header: 'Name', + footer: (props) => props.column.id, + columns: columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + footer: (props) => props.column.id, + }), + ]), + }), + columnHelper.group({ + header: 'Info', + footer: (props) => props.column.id, + columns: columnHelper.columns([ + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + }), + columnHelper.group({ + header: 'More Info', + columns: columnHelper.columns([ + columnHelper.accessor('visits', { + header: () => Visits, + footer: (props) => props.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + }), + ]), + }), + ]), + }), +]) + +function App() @{ + const [data, setData] = useState(() => makeData(20)) + const refreshData = () => setData(makeData(20)) + const stressTest = () => setData(makeData(1_000)) + + const table = useTable( + { + features, + columns, + data, + // initialState: { columnVisibility: { visits: false } }, // hide columns on first render + // atoms: { columnVisibility: columnVisibilityAtom }, // preferred: own visibility state with an external atom + // state: { columnVisibility }, // classic controlled state; pair with onColumnVisibilityChange + // onColumnVisibilityChange: setColumnVisibility, + // enableHiding: false, // prevent every column from being hidden; default true + debugTable: true, + debugHeaders: true, + debugColumns: true, + }, + (state) => state, // default selector + ) + +
+
+ + +
+
+
+
+ +
+ {table.getAllLeafColumns().map((column) => { + return ( +
+ +
+ ) + })} +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + + + {table.getFooterGroups().map((footerGroup) => ( + + {footerGroup.headers.map((header) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( + + )} +
+ +
+ {header.isPlaceholder ? null : ( + + )} +
+
+
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/column-visibility/src/makeData.ts b/examples/octane/column-visibility/src/makeData.ts new file mode 100644 index 0000000000..c0f87b8b19 --- /dev/null +++ b/examples/octane/column-visibility/src/makeData.ts @@ -0,0 +1,46 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + status: string + progress: number +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/column-visibility/src/vite-env.d.ts b/examples/octane/column-visibility/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/column-visibility/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/column-visibility/tests/e2e/smoke.spec.ts b/examples/octane/column-visibility/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..616e9ef589 --- /dev/null +++ b/examples/octane/column-visibility/tests/e2e/smoke.spec.ts @@ -0,0 +1,262 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +/** Leaf columns in render order. The toggle panel labels each by column id. */ +const LEAF_COLUMNS = [ + 'firstName', + 'lastName', + 'age', + 'visits', + 'status', + 'progress', +] + +/** Leaf header text differs from column id wherever a custom header is set. */ +const LEAF_HEADERS = [ + 'firstName', + 'Last Name', + 'Age', + 'Visits', + 'Status', + 'Profile Progress', +] + +let server: Awaited> | undefined + +test.beforeAll(async () => { + // One server for the whole file. A cold Vite start costs more than a test. + test.setTimeout(180_000) + server = await startExampleServer(exampleDir) +}) + +test.afterAll(async () => { + await server?.close() +}) + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + if (!server) throw new Error('Example server failed to start') + + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return errors +} + +function columnToggle(page: Page, label: string) { + return page.getByLabel(label, { exact: true }) +} + +/** Header rows run group to leaf, so the leaf row is last. */ +function leafHeaderCells(page: Page) { + return page.locator('thead tr').last().locator('th') +} + +/** Footer groups are the header groups reversed, so the leaf row is first. */ +function leafFooterCells(page: Page) { + return page.locator('tfoot tr').first().locator('th') +} + +function groupHeaderCells(page: Page) { + return page.locator('thead tr').first().locator('th') +} + +function firstBodyRowCells(page: Page) { + return page.locator('tbody tr').first().locator('td') +} + +/** Row data is random faker output, so `table.state` is the stable oracle. */ +async function readColumnVisibility(page: Page) { + const text = await page.getByTestId('table-state').textContent() + const state = JSON.parse(text ?? '{}') as { + columnVisibility?: Record + } + + return state.columnVisibility ?? {} +} + +async function expectColumnVisibility( + page: Page, + expected: Record, +) { + await expect.poll(() => readColumnVisibility(page)).toEqual(expected) +} + +async function getFirstBodyRowText(page: Page) { + const text = await page.locator('tbody tr').first().textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const errors = await openExample(page) + const table = page.locator('table').first() + + await expect(table).toBeVisible() + // Two nested column groups produce three header rows. + await expect(page.locator('thead tr')).toHaveCount(3) + await expect(groupHeaderCells(page)).toHaveText(['Name', 'Info']) + await expect(leafHeaderCells(page)).toHaveText(LEAF_HEADERS) + await expect(leafFooterCells(page)).toHaveText(LEAF_COLUMNS) + await expect(page.locator('tbody tr')).toHaveCount(20) + await expect(firstBodyRowCells(page)).toHaveCount(6) + await expectColumnVisibility(page, {}) + + expect(errors).toEqual([]) +}) + +test('regenerates table data', async ({ page }) => { + const errors = await openExample(page) + const regenerateButton = page.getByRole('button', { + name: /^Regenerate Data$/i, + }) + + await expect(page.locator('tbody tr').first()).toBeVisible() + + const firstRowBefore = await getFirstBodyRowText(page) + + await regenerateButton.click() + + await expect.poll(() => getFirstBodyRowText(page)).not.toBe(firstRowBefore) + await expect(page.locator('tbody tr')).toHaveCount(20) + + expect(errors).toEqual([]) +}) + +test('renders a toggle for every leaf column', async ({ page }) => { + const errors = await openExample(page) + + // One checkbox per leaf column plus the panel's Toggle All. + await expect(page.locator('.column-toggle-panel input')).toHaveCount(7) + await expect(page.locator('.column-toggle-row label')).toHaveText( + LEAF_COLUMNS, + ) + + await expect(columnToggle(page, 'Toggle All')).toBeChecked() + + for (const column of LEAF_COLUMNS) { + await expect(columnToggle(page, column)).toBeChecked() + } + + expect(errors).toEqual([]) +}) + +test('hides and shows an individual column', async ({ page }) => { + const errors = await openExample(page) + + await columnToggle(page, 'visits').uncheck() + + await expectColumnVisibility(page, { visits: false }) + await expect(leafHeaderCells(page)).toHaveText( + LEAF_HEADERS.filter((header) => header !== 'Visits'), + ) + await expect(firstBodyRowCells(page)).toHaveCount(5) + await expect(leafFooterCells(page)).toHaveCount(5) + // Toggle All is driven by `getIsAllColumnsVisible`, so it clears too. + await expect(columnToggle(page, 'Toggle All')).not.toBeChecked() + + await columnToggle(page, 'visits').check() + + // Re-showing records `true` rather than dropping the key. + await expectColumnVisibility(page, { visits: true }) + await expect(leafHeaderCells(page)).toHaveText(LEAF_HEADERS) + await expect(firstBodyRowCells(page)).toHaveCount(6) + await expect(columnToggle(page, 'Toggle All')).toBeChecked() + + expect(errors).toEqual([]) +}) + +test('removes a header group when all of its columns are hidden', async ({ + page, +}) => { + const errors = await openExample(page) + + await expect(groupHeaderCells(page)).toHaveText(['Name', 'Info']) + + await columnToggle(page, 'firstName').uncheck() + await expect(groupHeaderCells(page)).toHaveText(['Name', 'Info']) + + // The group header only disappears once its last child is hidden. + await columnToggle(page, 'lastName').uncheck() + await expect(groupHeaderCells(page)).toHaveText(['Info']) + await expect(leafHeaderCells(page)).toHaveText([ + 'Age', + 'Visits', + 'Status', + 'Profile Progress', + ]) + await expect(firstBodyRowCells(page)).toHaveCount(4) + + await columnToggle(page, 'firstName').check() + await expect(groupHeaderCells(page)).toHaveText(['Name', 'Info']) + + expect(errors).toEqual([]) +}) + +test('hides and shows every column with Toggle All', async ({ page }) => { + const errors = await openExample(page) + + await columnToggle(page, 'Toggle All').uncheck() + + await expectColumnVisibility( + page, + Object.fromEntries(LEAF_COLUMNS.map((column) => [column, false])), + ) + await expect(leafHeaderCells(page)).toHaveCount(0) + await expect(firstBodyRowCells(page)).toHaveCount(0) + // The rows survive; they simply have no visible cells left to render. + await expect(page.locator('tbody tr')).toHaveCount(20) + + for (const column of LEAF_COLUMNS) { + await expect(columnToggle(page, column)).not.toBeChecked() + } + + await columnToggle(page, 'Toggle All').check() + + await expectColumnVisibility( + page, + Object.fromEntries(LEAF_COLUMNS.map((column) => [column, true])), + ) + await expect(leafHeaderCells(page)).toHaveText(LEAF_HEADERS) + await expect(firstBodyRowCells(page)).toHaveCount(6) + + expect(errors).toEqual([]) +}) + +test('keeps column visibility when data is regenerated', async ({ page }) => { + const errors = await openExample(page) + + await columnToggle(page, 'age').uncheck() + await expect(firstBodyRowCells(page)).toHaveCount(5) + + // Visibility lives in table state, so replacing the rows must not reset it. + await page.getByRole('button', { name: /^Regenerate Data$/i }).click() + + await expectColumnVisibility(page, { age: false }) + await expect(columnToggle(page, 'age')).not.toBeChecked() + await expect(firstBodyRowCells(page)).toHaveCount(5) + await expect(leafHeaderCells(page)).toHaveText( + LEAF_HEADERS.filter((header) => header !== 'Age'), + ) + + expect(errors).toEqual([]) +}) diff --git a/examples/octane/column-visibility/tsconfig.json b/examples/octane/column-visibility/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/column-visibility/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/column-visibility/vite.config.ts b/examples/octane/column-visibility/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/column-visibility/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/composable-tables/index.html b/examples/octane/composable-tables/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/composable-tables/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/composable-tables/package.json b/examples/octane/composable-tables/package.json new file mode 100644 index 0000000000..f0777c39fa --- /dev/null +++ b/examples/octane/composable-tables/package.json @@ -0,0 +1,24 @@ +{ + "name": "@tanstack/octane-table-example-composable-tables", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-store": "^0.12.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/composable-tables/src/components/cell-components.tsrx b/examples/octane/composable-tables/src/components/cell-components.tsrx new file mode 100644 index 0000000000..aeccb51015 --- /dev/null +++ b/examples/octane/composable-tables/src/components/cell-components.tsrx @@ -0,0 +1,122 @@ +/** + * Cell-level components that use useCellContext + * + * These components can be used via the pre-bound cellComponents + * in AppCell children, e.g., + */ +import { useCellContext } from '../hooks/table' +import { IndeterminateCheckbox } from './indeterminate-checkbox.tsrx' + +/** + * Row-selection checkbox cell - toggles selection for the current row. + * + * Unlike React (which needs a `Subscribe` boundary to work around React + * Compiler memoization), Octane re-renders this cell when selection state + * changes via the parent table's subscription, so reading `row.getIsSelected()` + * directly is enough. + */ +export function SelectCell() @{ + const cell = useCellContext() + const row = cell.row + + +} + +/** + * Generic text cell renderer + */ +export function TextCell() @{ + const cell = useCellContext() + {cell.getValue()} +} + +/** + * Number cell with locale formatting + */ +export function NumberCell() @{ + const cell = useCellContext() + {cell.getValue().toLocaleString()} +} + +/** + * Status badge cell for status column + */ +export function StatusCell() @{ + const cell = useCellContext<'relationship' | 'complicated' | 'single'>() + const status = cell.getValue() + {status} +} + +/** + * Progress bar cell + */ +export function ProgressCell() @{ + const cell = useCellContext() + const progress = cell.getValue() +
+
+
+} + +/** + * Row actions cell - actions for the current row + */ +export function RowActionsCell() @{ + const cell = useCellContext() + const row = cell.row + +
+ + + +
+} + +/** + * Price cell with currency formatting + */ +export function PriceCell() @{ + const cell = useCellContext() + + $ + {cell.getValue().toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} + +} + +/** + * Category badge cell + */ +export function CategoryCell() @{ + const cell = useCellContext<'electronics' | 'clothing' | 'food' | 'books'>() + const category = cell.getValue() + {category} +} diff --git a/examples/octane/composable-tables/src/components/header-components.tsrx b/examples/octane/composable-tables/src/components/header-components.tsrx new file mode 100644 index 0000000000..dcba181534 --- /dev/null +++ b/examples/octane/composable-tables/src/components/header-components.tsrx @@ -0,0 +1,66 @@ +/** + * Header-level components that use useHeaderContext + * + * These components can be used via the pre-bound headerComponents + * in AppHeader children, e.g., + */ +import { useHeaderContext } from '../hooks/table' + +/** + * Sort indicator showing current sort direction + */ +export function SortIndicator() @{ + const header = useHeaderContext() + const sorted = header.column.getIsSorted() + + if (!sorted) return null + + {sorted === 'asc' ? '🔼' : '🔽'} +} + +/** + * Column filter input + */ +export function ColumnFilter() @{ + const header = useHeaderContext() + + if (!header.column.getCanFilter()) return null + + const columnFilterValue = header.column.getFilterValue() + +
e.stopPropagation()}> + + header.column.setFilterValue((e.target as HTMLInputElement).value) + } + placeholder={`Filter ${header.column.id}...`} + /> +
+} + +/** + * Footer showing the column ID + */ +export function FooterColumnId() @{ + const header = useHeaderContext() + {header.column.id} +} + +/** + * Footer showing a summary/aggregation for numeric columns + */ +export function FooterSum() @{ + const header = useHeaderContext() + const table = header.getContext().table + const rows = table.getFilteredRowModel().rows + + // Calculate sum for numeric columns + const sum = rows.reduce((acc, row) => { + const value = row.getValue(header.column.id) + return acc + (typeof value === 'number' ? value : 0) + }, 0) + + {sum > 0 ? sum.toLocaleString() : '—'} +} diff --git a/examples/octane/composable-tables/src/components/indeterminate-checkbox.tsrx b/examples/octane/composable-tables/src/components/indeterminate-checkbox.tsrx new file mode 100644 index 0000000000..0f77031e8a --- /dev/null +++ b/examples/octane/composable-tables/src/components/indeterminate-checkbox.tsrx @@ -0,0 +1,37 @@ +/** + * Indeterminate checkbox used by the row-selection column + * (both the select-all header and the per-row select cell). + */ +import { useEffect, useRef } from 'octane' + +export function IndeterminateCheckbox({ + indeterminate, + className = '', + checked, + onChange, + disabled, + ...rest +}: { + indeterminate?: boolean + checked?: boolean + disabled?: boolean + onChange?: (event: Event) => void +} & Record) @{ + const ref = useRef(null!) + + useEffect(() => { + if (typeof indeterminate === 'boolean') { + ref.current.indeterminate = !checked && indeterminate + } + }, [ref, indeterminate, checked]) + + +} diff --git a/examples/octane/composable-tables/src/components/table-components.tsrx b/examples/octane/composable-tables/src/components/table-components.tsrx new file mode 100644 index 0000000000..164265f736 --- /dev/null +++ b/examples/octane/composable-tables/src/components/table-components.tsrx @@ -0,0 +1,117 @@ +/** + * Table-level components that use useTableContext + * + * These components can be used via the pre-bound tableComponents + * directly on the table object, e.g., + */ +import { useTableContext } from '../hooks/table' +import type { PaginationState } from '@tanstack/octane-table' + +/** + * Pagination controls for the table + */ +export function PaginationControls() @{ + const table = useTableContext() + +
+ + + + + + Page{' '} + + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {table.getPageCount().toLocaleString()} + + + + | Go to page: + { + const page = (e.target as HTMLInputElement).value + ? Number((e.target as HTMLInputElement).value) - 1 + : 0 + table.setPageIndex(page) + }} + /> + + +
+} + +/** + * Row count display + */ +export function RowCount() @{ + const table = useTableContext() + +
+ Showing {table.getRowModel().rows.length.toLocaleString()} of{' '} + {table.getRowCount().toLocaleString()} rows +
+} + +/** + * Table toolbar with title and actions + */ +export function TableToolbar({ + title, + onRefresh, + onStressTest, +}: { + title: string + onRefresh?: () => void + onStressTest?: () => void +}) @{ + const table = useTableContext() + +
+

{title}

+
+ {onRefresh && } + {onStressTest && ( + + )} + + +
+
+} diff --git a/examples/octane/composable-tables/src/hooks/table.ts b/examples/octane/composable-tables/src/hooks/table.ts new file mode 100644 index 0000000000..70a2815b99 --- /dev/null +++ b/examples/octane/composable-tables/src/hooks/table.ts @@ -0,0 +1,115 @@ +/** + * Custom table hook setup using createTableHook + * + * This file creates a custom useAppTable hook with pre-bound components. + * Features, row models, and default options are defined once here and shared across all tables. + * Context hooks and a pre-bound createAppColumnHelper are also exported. + */ +import { + columnFilteringFeature, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + createTableHook, + filterFn_inNumberRange, + filterFn_includesString, + rowPaginationFeature, + rowSelectionFeature, + rowSortingFeature, + sortFn_alphanumeric, + sortFn_text, + tableFeatures, +} from '@tanstack/octane-table' + +// Import table-level components +import { + PaginationControls, + RowCount, + TableToolbar, +} from '../components/table-components.tsrx' + +// Import cell-level components +import { + CategoryCell, + NumberCell, + PriceCell, + ProgressCell, + RowActionsCell, + SelectCell, + StatusCell, + TextCell, +} from '../components/cell-components.tsrx' + +// Import header/footer-level components (both use useHeaderContext) +import { + ColumnFilter, + FooterColumnId, + FooterSum, + SortIndicator, +} from '../components/header-components.tsrx' + +/** + * Create the custom table hook with all pre-bound components. + * This exports: + * - createAppColumnHelper: Create column definitions with TFeatures already bound + * - useAppTable: Hook for creating tables with TFeatures baked in + * - useTableContext: Access table instance in tableComponents + * - useCellContext: Access cell instance in cellComponents + * - useHeaderContext: Access header instance in headerComponents + */ +export const { + createAppColumnHelper, + useAppTable, + useTableContext, + useCellContext, + useHeaderContext, +} = createTableHook({ + // Features are set once here and shared across all tables + features: tableFeatures({ + columnFilteringFeature, + rowPaginationFeature, + rowSelectionFeature, + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortFns: { + alphanumeric: sortFn_alphanumeric, + text: sortFn_text, + }, + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, + }), + + // set any default table options here too + getRowId: (row) => row.id, + + // Register table-level components (accessible via table.ComponentName) + tableComponents: { + PaginationControls, + RowCount, + TableToolbar, + }, + + // Register cell-level components (accessible via cell.ComponentName in AppCell) + cellComponents: { + SelectCell, + TextCell, + NumberCell, + StatusCell, + ProgressCell, + RowActionsCell, + PriceCell, + CategoryCell, + }, + + // Register header/footer-level components (accessible via header.ComponentName in AppHeader/AppFooter) + headerComponents: { + SortIndicator, + ColumnFilter, + FooterColumnId, + FooterSum, + }, +}) diff --git a/examples/octane/composable-tables/src/index.css b/examples/octane/composable-tables/src/index.css new file mode 100644 index 0000000000..521b9c190c --- /dev/null +++ b/examples/octane/composable-tables/src/index.css @@ -0,0 +1,620 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + width: 100%; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th, +td { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 8px 12px; + text-align: left; +} + +th { + background-color: #f5f5f5; + font-weight: 600; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +.table-container { + padding: 16px; + max-width: 1200px; + margin: 0 auto; + border-collapse: collapse; + border-spacing: 0; +} + +/* Fixed-height, scrollable region around the table. With a page size larger + than ~12 rows the body overflows and scrolls; this is what makes any + remounting of the App wrappers visible (the scroll position would reset). */ +.table-scroll { + max-height: 400px; + overflow: auto; + border: 1px solid lightgray; +} + +.table-scroll thead th { + position: sticky; + top: 0; + z-index: 1; +} + +.pagination { + display: flex; + align-items: center; + gap: 8px; + margin-top: 16px; + flex-wrap: wrap; +} + +.pagination button { + border: 1px solid #ccc; + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + background: white; +} + +.pagination button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.pagination input { + border: 1px solid #ccc; + border-radius: 4px; + padding: 4px; + width: 64px; +} + +.pagination select { + border: 1px solid #ccc; + border-radius: 4px; + padding: 4px; +} + +.sort-indicator { + margin-left: 4px; +} + +.column-filter { + margin-top: 4px; +} + +.column-filter input { + border: 1px solid #ccc; + border-radius: 4px; + padding: 4px; + width: 100%; + font-size: 12px; +} + +.sortable-header { + cursor: pointer; + user-select: none; +} + +.sortable-header:hover { + background-color: #e8e8e8; +} + +.row-actions { + display: flex; + gap: 4px; +} + +.row-actions button { + border: 1px solid #ccc; + border-radius: 4px; + padding: 2px 8px; + cursor: pointer; + background: white; + font-size: 12px; +} + +.row-actions button:hover { + background-color: #f0f0f0; +} + +.status-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.status-badge.relationship { + background-color: #d4edda; + color: #155724; +} + +.status-badge.complicated { + background-color: #fff3cd; + color: #856404; +} + +.status-badge.single { + background-color: #cce5ff; + color: #004085; +} + +.progress-bar { + width: 100%; + height: 8px; + background-color: #e9ecef; + border-radius: 4px; + overflow: hidden; +} + +.progress-bar-fill { + height: 100%; + background-color: #007bff; + transition: width 0.2s; +} + +.table-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + flex-wrap: wrap; + gap: 8px; + border-collapse: collapse; + border-spacing: 0; +} + +.table-toolbar h2 { + margin: 0; + border-collapse: collapse; + border-spacing: 0; +} + +.table-toolbar-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.table-toolbar button { + border: 1px solid #ccc; + border-radius: 4px; + padding: 8px 16px; + cursor: pointer; + background: white; + border-collapse: collapse; + border-spacing: 0; +} + +.table-toolbar button:hover { + background-color: #f0f0f0; + border-collapse: collapse; + border-spacing: 0; +} + +.row-count { + color: #666; + font-size: 14px; + margin-top: 8px; +} + +.app { + padding: 16px; +} + +.app h1 { + text-align: center; + margin-bottom: 8px; +} + +.description { + text-align: center; + color: #666; + margin-bottom: 32px; +} + +.description code { + background-color: #f5f5f5; + padding: 2px 6px; + border-radius: 4px; + font-size: 13px; +} + +.table-divider { + height: 48px; + border-bottom: 1px solid #e0e0e0; + margin: 32px auto; + max-width: 1200px; + border-collapse: collapse; + border-spacing: 0; +} + +.category-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + text-transform: capitalize; +} + +.category-badge.electronics { + background-color: #e3f2fd; + color: #1565c0; +} + +.category-badge.clothing { + background-color: #fce4ec; + color: #c2185b; +} + +.category-badge.food { + background-color: #e8f5e9; + color: #2e7d32; +} + +.category-badge.books { + background-color: #fff8e1; + color: #f57c00; +} + +.price { + font-weight: 600; + color: #2e7d32; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/composable-tables/src/main.tsrx b/examples/octane/composable-tables/src/main.tsrx new file mode 100644 index 0000000000..2e1cf797e1 --- /dev/null +++ b/examples/octane/composable-tables/src/main.tsrx @@ -0,0 +1,446 @@ +import { useCallback, useMemo, useState } from 'octane' +import { createRoot } from 'octane' +import { createAppColumnHelper, useAppTable } from './hooks/table' +import { IndeterminateCheckbox } from './components/indeterminate-checkbox.tsrx' +import { makeData, makeProductData } from './makeData' +import type { Person, Product } from './makeData' +import './index.css' +// Import cell components directly - they use useCellContext internally + +// Create column helpers with TFeatures already bound - only need TData! +const personColumnHelper = createAppColumnHelper() +const productColumnHelper = createAppColumnHelper() + +// Users Table Component - Original implementation +function UsersTable() @{ + // Data state + const [data, setData] = useState(() => makeData(1000)) + + // Refresh data callback + const refreshData = useCallback(() => { + setData(makeData(1_000)) + }, []) + + const stressTest = useCallback(() => { + setData(makeData(1_000_000)) + }, []) + + // Define columns using the column helper + const columns = useMemo( + () => + // NOTE: You must use `createAppColumnHelper` instead of `createColumnHelper` when using pre-bound components like + personColumnHelper.columns([ + personColumnHelper.display({ + id: 'select', + header: ({ table }) => ( + + ), + // Cell uses the pre-bound SelectCell component via AppCell + cell: ({ cell }) => , + }), + personColumnHelper.accessor('firstName', { + header: 'First Name', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + personColumnHelper.accessor('lastName', { + header: 'Last Name', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + personColumnHelper.accessor('age', { + header: 'Age', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + personColumnHelper.accessor('visits', { + header: 'Visits', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + personColumnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + personColumnHelper.accessor('progress', { + header: 'Progress', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + personColumnHelper.display({ + id: 'actions', + header: 'Actions', + cell: ({ cell }) => , + }), + ]), + [], + ) + + // Create the table - features and rowModels are already configured! + const table = useAppTable( + { + columns, + data, + debugTable: true, + enableRowSelection: true, + // more table options + }, + (state) => state, // default selector + ) + + +
+ {/* Table toolbar using pre-bound component */} + + + {/* Scroll container with a fixed max-height so larger page sizes + scroll. If the App wrappers remount on state updates, the scroll + position jumps back to the top on every keystroke/selection. */} +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((h) => ( + + {(header) => ( + + )} + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getAllCells().map((c) => ( + + {(cell) => ( + + )} + + ))} + + ))} + + + {table.getFooterGroups().map((footerGroup) => ( + + {footerGroup.headers.map((f) => ( + + {(footer) => { + const columnId = footer.column.id + const hasFilter = table.state.columnFilters.some( + (cf) => cf.id === columnId, + ) + + return ( + + ) + }} + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( + <> + + + + {/* Show sort order number when multiple columns sorted */} + {table.state.sorting.length > 1 && + table.state.sorting.findIndex( + (s) => s.id === header.column.id, + ) > -1 && ( + + {table.state.sorting.findIndex( + (s) => s.id === header.column.id, + ) + 1} + + )} + + )} +
+ {/* Cell components are pre-bound via AppCell */} + +
+ {footer.isPlaceholder ? null : ( + <> + {/* Use FooterSum for numeric columns, FooterColumnId for others */} + {columnId === 'age' || + columnId === 'visits' || + columnId === 'progress' ? ( + <> + + {hasFilter && ( + + {' '} + (filtered) + + )} + + ) : columnId === 'actions' ? null : ( + <> + + {hasFilter && ( + + {' '} + {'✓'} + + )} + + )} + + )} +
+
+ + {/* Pagination using pre-bound component */} + + + {/* Row count using pre-bound component */} + +
+
+} + +// Products Table Component - New implementation using same hook and components +function ProductsTable() @{ + // Data state + const [data, setData] = useState(() => makeProductData(500)) + + // Refresh data callback + const refreshData = useCallback(() => { + setData(makeProductData(500)) + }, []) + + const stressTest = useCallback(() => { + setData(makeProductData(1_000_000)) + }, []) + + // Define columns using the column helper - different structure than Users table + const columns = useMemo( + () => + productColumnHelper.columns([ + productColumnHelper.display({ + id: 'select', + header: ({ table }) => ( + + ), + // Cell uses the pre-bound SelectCell component via AppCell + cell: ({ cell }) => , + }), + productColumnHelper.accessor('name', { + header: 'Product Name', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + productColumnHelper.accessor('category', { + header: 'Category', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + productColumnHelper.accessor('price', { + header: 'Price', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + productColumnHelper.accessor('stock', { + header: 'In Stock', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + productColumnHelper.accessor('rating', { + header: 'Rating', + footer: (props) => props.column.id, + cell: ({ cell }) => , + }), + ]), + [], + ) + + // Create the table using the same useAppTable hook + const table = useAppTable( + { + debugTable: true, + columns, + data, + getRowId: (row) => row.id, + enableRowSelection: true, + }, + (state) => state, // default selector + ) + + +
+ {/* Table toolbar using the same pre-bound component */} + + + {/* Scroll container with a fixed max-height so larger page sizes + scroll. If the App wrappers remount on state updates, the scroll + position jumps back to the top on every keystroke/selection. */} +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((h) => ( + + {(header) => ( + + )} + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getAllCells().map((c) => ( + + {(cell) => ( + + )} + + ))} + + ))} + + + {table.getFooterGroups().map((footerGroup) => ( + + {footerGroup.headers.map((f) => ( + + {(footer) => { + const columnId = footer.column.id + const hasFilter = table.state.columnFilters.some( + (cf) => cf.id === columnId, + ) + + return ( + + ) + }} + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( + <> + + + + {table.state.sorting.length > 1 && + table.state.sorting.findIndex( + (s) => s.id === header.column.id, + ) > -1 && ( + + {table.state.sorting.findIndex( + (s) => s.id === header.column.id, + ) + 1} + + )} + + )} +
+ {/* Cell components are pre-bound via AppCell */} + +
+ {footer.isPlaceholder ? null : ( + <> + {/* Use FooterSum for numeric columns, FooterColumnId for others */} + {columnId === 'price' || + columnId === 'stock' || + columnId === 'rating' ? ( + <> + + {hasFilter && ( + + {' '} + (filtered) + + )} + + ) : ( + <> + + {hasFilter && ( + + {' '} + {'✓'} + + )} + + )} + + )} +
+
+ + {/* Pagination using the same pre-bound component */} + + + {/* Row count using the same pre-bound component */} + +
+
+} + +function App() @{ +
+

Composable Tables Example

+

+ Both tables below use the same useAppTable hook and + shareable components, but with different data types and column + configurations. +

+ + {/* Original Users Table */} + + +
+ + {/* New Products Table */} + +
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/composable-tables/src/makeData.ts b/examples/octane/composable-tables/src/makeData.ts new file mode 100644 index 0000000000..17dec1c6de --- /dev/null +++ b/examples/octane/composable-tables/src/makeData.ts @@ -0,0 +1,77 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +export type Product = { + id: string + name: string + category: 'electronics' | 'clothing' | 'food' | 'books' + price: number + stock: number + rating: number +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +const newProduct = (): Product => { + return { + id: faker.string.uuid(), + name: faker.commerce.productName(), + category: faker.helpers.shuffle([ + 'electronics', + 'clothing', + 'food', + 'books', + ])[0], + price: parseFloat(faker.commerce.price({ min: 5, max: 500 })), + stock: faker.number.int({ min: 0, max: 200 }), + rating: faker.number.int({ min: 0, max: 100 }), + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} + +export function makeProductData(count: number): Array { + return range(count).map(() => newProduct()) +} diff --git a/examples/octane/composable-tables/src/vite-env.d.ts b/examples/octane/composable-tables/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/composable-tables/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/composable-tables/tests/e2e/smoke.spec.ts b/examples/octane/composable-tables/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..868e818817 --- /dev/null +++ b/examples/octane/composable-tables/tests/e2e/smoke.spec.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' +import path from 'node:path' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return { errors, server } +} + +function getTable(page: Page) { + return page + .locator('table:visible, .divTable:visible') + .filter({ has: page.locator('thead th, .thead .th') }) + .filter({ has: page.locator('tbody tr, .tbody .tr') }) + .first() +} + +function getHeaderCells(table: Locator) { + return table.locator('thead th, .thead .th') +} + +function getBodyRows(table: Locator) { + return table.locator('tbody tr, .tbody .tr') +} + +async function getFirstBodyRowData(table: Locator) { + const row = getBodyRows(table).first() + const firstInput = row + .locator('input:not([type="checkbox"]):not([type="radio"])') + .first() + + if ((await firstInput.count()) > 0 && (await firstInput.isVisible())) { + return firstInput.inputValue() + } + + const text = await row.textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const { errors, server } = await openExample(page) + + try { + const table = getTable(page) + const bodyRows = getBodyRows(table) + + await expect(table).toBeVisible() + await expect(getHeaderCells(table).first()).toBeVisible() + await expect(bodyRows.first()).toBeVisible() + + const regenerateButton = page + .getByRole('button', { name: /^Regenerate Data$/i }) + .first() + + if ((await regenerateButton.count()) > 0) { + await expect(regenerateButton).toBeVisible() + + const firstRowBefore = await getFirstBodyRowData(table) + + await regenerateButton.click() + + await expect + .poll(() => getFirstBodyRowData(table)) + .not.toBe(firstRowBefore) + await expect(bodyRows.first()).toBeVisible() + } + + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/composable-tables/tsconfig.json b/examples/octane/composable-tables/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/composable-tables/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/composable-tables/vite.config.ts b/examples/octane/composable-tables/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/composable-tables/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/custom-plugin/index.html b/examples/octane/custom-plugin/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/custom-plugin/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/custom-plugin/package.json b/examples/octane/custom-plugin/package.json new file mode 100644 index 0000000000..5d5e33b6ba --- /dev/null +++ b/examples/octane/custom-plugin/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-custom-plugin", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/custom-plugin/src/index.css b/examples/octane/custom-plugin/src/index.css new file mode 100644 index 0000000000..f8ffbb8e90 --- /dev/null +++ b/examples/octane/custom-plugin/src/index.css @@ -0,0 +1,373 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +tr { + border-bottom: 1px solid lightgray; +} + +button:disabled { + opacity: 0.5; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/custom-plugin/src/main.tsrx b/examples/octane/custom-plugin/src/main.tsrx new file mode 100644 index 0000000000..95aee6421e --- /dev/null +++ b/examples/octane/custom-plugin/src/main.tsrx @@ -0,0 +1,415 @@ +import { useMemo, useState } from 'octane' +import { createRoot } from 'octane' +import './index.css' +import { + assignTableAPIs, + columnFilteringFeature, + createColumnHelper, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + filterFn_inNumberRange, + filterFn_includesString, + functionalUpdate, + makeStateUpdater, + rowPaginationFeature, + rowSortingFeature, + sortFn_alphanumeric, + sortFn_text, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { + Column, + OnChangeFn, + OctaneTable, + RowData, + TableFeature, + TableFeatures, + Updater, +} from '@tanstack/octane-table' +import type { Person } from './makeData' + +// TypeScript setup for our new feature with all of the same type-safety as stock TanStack Table features + +// 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 +} + +declare module '@tanstack/octane-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 + } +} + +// Here is all of the actual javascript code for our new feature +export const densityPlugin: TableFeature = { + // define the new feature's initial state + getInitialState: (initialState) => { + return { + density: 'md', + ...initialState, // must come last + } + }, + + // define the new feature's default options + getDefaultTableOptions: (table) => { + return { + enableDensity: true, + onDensityChange: makeStateUpdater('density', table), + } + }, + // if you need to add a default column definition... + // getDefaultColumnDef: () => {}, + + // define the new feature's table instance methods + constructTableAPIs: (table) => { + assignTableAPIs('densityPlugin', table, { + table_setDensity: { + fn: (updater: Updater) => { + const safeUpdater: Updater = (old) => { + const newState = functionalUpdate(updater, old) + return newState + } + return (table.options as TableOptions_Density).onDensityChange?.( + safeUpdater, + ) + }, + }, + table_toggleDensity: { + fn: (value?: DensityState) => { + const safeUpdater: Updater = (old) => { + if (value) return value + return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' // cycle through the 3 options + } + return (table.options as TableOptions_Density).onDensityChange?.( + safeUpdater, + ) + }, + }, + }) + }, +} +// end of custom feature code + +// app code +const features = tableFeatures({ + columnFilteringFeature, + rowSortingFeature, + rowPaginationFeature, + densityPlugin, // pass in our plugin just like any other stock feature + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortedRowModel: createSortedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, + sortFns: { + alphanumeric: sortFn_alphanumeric, + text: sortFn_text, + }, +}) + +const columnHelper = createColumnHelper() + +function App() @{ + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + footer: (props) => props.column.id, + }), + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + }), + columnHelper.accessor('visits', { + header: () => Visits, + footer: (props) => props.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + }), + ]), + [], + ) + + const [data, setData] = useState(() => makeData(1_000)) + const refreshData = () => setData(makeData(1_000)) + const stressTest = () => setData(makeData(1_000_000)) + const [density, setDensity] = useState('md') + + const table = useTable( + { + features, + columns, + data, + debugTable: true, + state: { + density, // passing the density state to the table, TS is still happy :) + }, + onDensityChange: setDensity, // using the new onDensityChange option, TS is still happy :) + }, + (state) => state, // default selector + ) + +
+
+ + +
+
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + return ( + + {row.getAllCells().map((cell) => { + return ( + + ) + })} + + ) + })} + +
+
+ + {{ + asc: ' 🔼', + desc: ' 🔽', + }[header.column.getIsSorted() as string] ?? null} +
+ {header.column.getCanFilter() ? ( +
+ +
+ ) : null} +
+ +
+
+
+ + + + + +
Page
+ + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + { + const page = (e.target as HTMLInputElement).value + ? Number((e.target as HTMLInputElement).value) - 1 + : 0 + table.setPageIndex(page) + }} + className="page-size-input" + /> + + +
+
+ Showing {table.getRowModel().rows.length.toLocaleString()} of{' '} + {String(table.getRowCount().toLocaleString())} Rows +
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +function Filter({ + column, + table, +}: { + column: Column + table: OctaneTable +}) { + const firstValue = table + .getPreFilteredRowModel() + .flatRows[0]?.getValue(column.id) + + const columnFilterValue = column.getFilterValue() + + return typeof firstValue === 'number' ? ( +
+ + column.setFilterValue((old: [number, number]) => [ + (e.target as HTMLInputElement).value, + old[1], + ]) + } + placeholder={`Min`} + className="filter-input" + /> + + column.setFilterValue((old: [number, number]) => [ + old[0], + (e.target as HTMLInputElement).value, + ]) + } + placeholder={`Max`} + className="filter-input" + /> +
+ ) : ( + + column.setFilterValue((e.target as HTMLInputElement).value) + } + placeholder={`Search...`} + className="filter-select" + /> + ) +} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/custom-plugin/src/makeData.ts b/examples/octane/custom-plugin/src/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/octane/custom-plugin/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/custom-plugin/src/vite-env.d.ts b/examples/octane/custom-plugin/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/custom-plugin/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/custom-plugin/tests/e2e/smoke.spec.ts b/examples/octane/custom-plugin/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..868e818817 --- /dev/null +++ b/examples/octane/custom-plugin/tests/e2e/smoke.spec.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' +import path from 'node:path' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return { errors, server } +} + +function getTable(page: Page) { + return page + .locator('table:visible, .divTable:visible') + .filter({ has: page.locator('thead th, .thead .th') }) + .filter({ has: page.locator('tbody tr, .tbody .tr') }) + .first() +} + +function getHeaderCells(table: Locator) { + return table.locator('thead th, .thead .th') +} + +function getBodyRows(table: Locator) { + return table.locator('tbody tr, .tbody .tr') +} + +async function getFirstBodyRowData(table: Locator) { + const row = getBodyRows(table).first() + const firstInput = row + .locator('input:not([type="checkbox"]):not([type="radio"])') + .first() + + if ((await firstInput.count()) > 0 && (await firstInput.isVisible())) { + return firstInput.inputValue() + } + + const text = await row.textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const { errors, server } = await openExample(page) + + try { + const table = getTable(page) + const bodyRows = getBodyRows(table) + + await expect(table).toBeVisible() + await expect(getHeaderCells(table).first()).toBeVisible() + await expect(bodyRows.first()).toBeVisible() + + const regenerateButton = page + .getByRole('button', { name: /^Regenerate Data$/i }) + .first() + + if ((await regenerateButton.count()) > 0) { + await expect(regenerateButton).toBeVisible() + + const firstRowBefore = await getFirstBodyRowData(table) + + await regenerateButton.click() + + await expect + .poll(() => getFirstBodyRowData(table)) + .not.toBe(firstRowBefore) + await expect(bodyRows.first()).toBeVisible() + } + + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/custom-plugin/tsconfig.json b/examples/octane/custom-plugin/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/custom-plugin/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/custom-plugin/vite.config.ts b/examples/octane/custom-plugin/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/custom-plugin/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/expanding/index.html b/examples/octane/expanding/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/expanding/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/expanding/package.json b/examples/octane/expanding/package.json new file mode 100644 index 0000000000..ac72366bd9 --- /dev/null +++ b/examples/octane/expanding/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-expanding", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/expanding/src/index.css b/examples/octane/expanding/src/index.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/octane/expanding/src/index.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/expanding/src/main.tsrx b/examples/octane/expanding/src/main.tsrx new file mode 100644 index 0000000000..74db6976e8 --- /dev/null +++ b/examples/octane/expanding/src/main.tsrx @@ -0,0 +1,370 @@ +import { useEffect, useMemo, useRef, useState } from 'octane' +import { createRoot } from 'octane' +import { + columnFilteringFeature, + createColumnHelper, + createExpandedRowModel, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + filterFn_between, + filterFn_inNumberRange, + filterFn_includesString, + rowExpandingFeature, + rowPaginationFeature, + rowSelectionFeature, + rowSortingFeature, + sortFn_alphanumeric, + sortFn_text, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Octane } from 'octane/jsx-runtime' +import type { Person } from './makeData' +import type { Column, Table } from '@tanstack/octane-table' +import './index.css' + +const features = tableFeatures({ + columnFilteringFeature, + rowExpandingFeature, + rowPaginationFeature, + rowSortingFeature, + rowSelectionFeature, + expandedRowModel: createExpandedRowModel(), + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortedRowModel: createSortedRowModel(), + filterFns: { + between: filterFn_between, + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, + sortFns: { + alphanumeric: sortFn_alphanumeric, + text: sortFn_text, + }, +}) + +const columnHelper = createColumnHelper() + +function App() @{ + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.display({ + id: 'rowNumber', + header: '#', + cell: ({ row }) => row.getDisplayIndex() + 1, + }), + columnHelper.accessor('firstName', { + header: ({ table }) => ( + <> + {' '} + {' '} + First Name + + ), + cell: ({ row, getValue }) => ( +
+
+ {' '} + {row.getCanExpand() ? ( + + ) : ( + '🔵' + )}{' '} + {getValue()} +
+
+ ), + footer: (props) => props.column.id, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + footer: (props) => props.column.id, + }), + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + filterFn: 'between', + }), + columnHelper.accessor('visits', { + header: () => Visits, + footer: (props) => props.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + }), + ]), + [], + ) + + const [data, setData] = useState(() => makeData(100, 5, 3)) + const refreshData = () => setData(() => makeData(100, 5, 3)) + const stressTest = () => setData(() => makeData(10_000, 5, 3)) + + const table = useTable( + { + features, + columns, + data, + getSubRows: (row) => row.subRows, + // initialState: { expanded: { '0': true } }, // expand rows on first render + // atoms: { expanded: expandedAtom }, // preferred: own expanded state with an external atom + // state: { expanded }, // classic controlled state; pair with onExpandedChange + // onExpandedChange: setExpanded, + // enableExpanding: false, // disable expanding for every row; default true + // getRowCanExpand: row => row.original.subRows?.length > 0, // override which rows can expand + // getIsRowExpanded: row => row.id === '0', // override whether a row is expanded + // manualExpanding: true, // pass data that is already expanded, for example from a server + // paginateExpandedRows: false, // keep expanded children on their parent page; default true + // autoResetExpanded: false, // keep expanded rows after page-altering changes; default true + // autoResetAll: false, // turn off every feature's automatic reset, including expansion + // filterFromLeafRows: true, // with filtering, keep parents whose descendants match + // maxLeafRowFilterDepth: 0, // with filtering, only filter root rows + debugTable: true, + }, + (state) => state, // default selector + ) + +
+
+ + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + return ( + + {row.getAllCells().map((cell) => { + return ( + + ) + })} + + ) + })} + +
+ {header.isPlaceholder ? null : ( +
+ + {header.column.getCanFilter() ? ( +
+ +
+ ) : null} +
+ )} +
+ +
+
+
+ + + + + +
Page
+ + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + { + const page = (e.target as HTMLInputElement).value + ? Number((e.target as HTMLInputElement).value) - 1 + : 0 + table.setPageIndex(page) + }} + className="page-size-input" + /> + + +
+
{String(table.getRowModel().rows.length.toLocaleString())} Rows
+
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +function Filter({ + column, + table, +}: { + column: Column + table: Table +}) { + const firstValue = table + .getPreFilteredRowModel() + .flatRows[0]?.getValue(column.id) + + const columnFilterValue = column.getFilterValue() + + return typeof firstValue === 'number' ? ( +
+ + column.setFilterValue((old: [number, number] | undefined) => [ + (e.target as HTMLInputElement).value, + old?.[1], + ]) + } + placeholder={`Min`} + className="filter-input" + /> + + column.setFilterValue((old: [number, number] | undefined) => [ + old?.[0], + (e.target as HTMLInputElement).value, + ]) + } + placeholder={`Max`} + className="filter-input" + /> +
+ ) : ( + + column.setFilterValue((e.target as HTMLInputElement).value) + } + placeholder={`Search...`} + className="filter-select" + /> + ) +} + +function IndeterminateCheckbox({ + indeterminate, + className = '', + checked, + onChange, + ...rest +}: { + indeterminate?: boolean + checked?: boolean + onChange?: (event: Event) => void +} & Omit, 'checked' | 'onChange'>) @{ + const ref = useRef(null) + + useEffect(() => { + if (typeof indeterminate === 'boolean' && ref.current) { + ref.current.indeterminate = !checked && indeterminate + } + }, [ref, indeterminate, checked]) + + +} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/expanding/src/makeData.ts b/examples/octane/expanding/src/makeData.ts new file mode 100644 index 0000000000..95302cdf16 --- /dev/null +++ b/examples/octane/expanding/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/expanding/src/vite-env.d.ts b/examples/octane/expanding/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/expanding/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/expanding/tests/e2e/smoke.spec.ts b/examples/octane/expanding/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..d579559779 --- /dev/null +++ b/examples/octane/expanding/tests/e2e/smoke.spec.ts @@ -0,0 +1,228 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +/** `makeData(100, 5, 3)`: 100 roots, 5 children each, 3 grandchildren each. */ +const CHILDREN_PER_ROW = 5 +const PAGE_SIZE = 10 + +let server: Awaited> | undefined + +test.beforeAll(async () => { + // One server for the whole file. A cold Vite start costs more than a test. + test.setTimeout(180_000) + server = await startExampleServer(exampleDir) +}) + +test.afterAll(async () => { + await server?.close() +}) + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + if (!server) throw new Error('Example server failed to start') + + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return errors +} + +function bodyRows(page: Page) { + return page.locator('tbody tr') +} + +/** The expander lives in each row's first name cell, beside the checkbox. */ +function rowExpander(page: Page, index: number) { + return bodyRows(page).nth(index).getByRole('button') +} + +/** The wrapper whose padding encodes the row's depth. */ +function rowIndent(page: Page, index: number) { + return bodyRows(page).nth(index).locator('td').nth(1).locator('div').first() +} + +/** The expand-all control sits in the first name header. */ +function expandAllButton(page: Page) { + return page.locator('thead th').nth(1).getByRole('button') +} + +function rowCheckbox(page: Page, index: number) { + return bodyRows(page).nth(index).locator('input[type="checkbox"]').first() +} + +/** Row data is random faker output, so `table.state` is the stable oracle. */ +async function readState(page: Page) { + const text = await page.getByTestId('table-state').textContent() + + return JSON.parse(text ?? '{}') as { + expanded?: Record | true + rowSelection?: Record + } +} + +async function countExpanded(page: Page) { + const expanded = (await readState(page)).expanded + return expanded === true ? -1 : Object.keys(expanded ?? {}).length +} + +async function countSelected(page: Page) { + return Object.keys((await readState(page)).rowSelection ?? {}).length +} + +async function getRowText(page: Page, index: number) { + const text = await bodyRows(page).nth(index).textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const errors = await openExample(page) + const table = page.locator('table').first() + + await expect(table).toBeVisible() + await expect(bodyRows(page)).toHaveCount(PAGE_SIZE) + await expect.poll(async () => (await readState(page)).expanded).toEqual({}) + // Nothing is expanded yet, so every visible row is a root row. + await expect(rowExpander(page, 0)).toHaveText('👉') + await expect(rowIndent(page, 0)).toHaveCSS('padding-left', '0px') + + expect(errors).toEqual([]) +}) + +test('regenerates table data', async ({ page }) => { + const errors = await openExample(page) + const regenerateButton = page.getByRole('button', { + name: /^Regenerate Data$/i, + }) + + await expect(bodyRows(page).first()).toBeVisible() + + const firstRowBefore = await getRowText(page, 0) + + await regenerateButton.click() + + await expect.poll(() => getRowText(page, 0)).not.toBe(firstRowBefore) + await expect(bodyRows(page)).toHaveCount(PAGE_SIZE) + + expect(errors).toEqual([]) +}) + +test('expands and collapses a single row', async ({ page }) => { + const errors = await openExample(page) + const secondRowBefore = await getRowText(page, 1) + + await rowExpander(page, 0).click() + + await expect(rowExpander(page, 0)).toHaveText('👇') + await expect.poll(() => countExpanded(page)).toBe(1) + + // Children are spliced in beneath their parent, but pagination still hands + // back one page, so the rows below are pushed out of the window instead. + await expect(bodyRows(page)).toHaveCount(PAGE_SIZE) + expect(await getRowText(page, 1)).not.toBe(secondRowBefore) + await expect(rowIndent(page, 1)).toHaveCSS('padding-left', '28px') + + await rowExpander(page, 0).click() + + await expect(rowExpander(page, 0)).toHaveText('👉') + await expect.poll(() => countExpanded(page)).toBe(0) + // Collapsing restores exactly the row that was displaced. + expect(await getRowText(page, 1)).toBe(secondRowBefore) + + expect(errors).toEqual([]) +}) + +test('indents each level of sub rows', async ({ page }) => { + const errors = await openExample(page) + + await rowExpander(page, 0).click() + await expect(rowExpander(page, 0)).toHaveText('👇') + + // Depth drives a 2rem indent per level, and this example sets the root font + // size to 14px, so each level adds 28px. + await expect(rowIndent(page, 0)).toHaveCSS('padding-left', '0px') + await expect(rowIndent(page, 1)).toHaveCSS('padding-left', '28px') + + await rowExpander(page, 1).click() + + await expect(rowExpander(page, 1)).toHaveText('👇') + await expect(rowIndent(page, 2)).toHaveCSS('padding-left', '56px') + await expect.poll(() => countExpanded(page)).toBe(2) + + expect(errors).toEqual([]) +}) + +test('expands every row from the header', async ({ page }) => { + const errors = await openExample(page) + + await expect(expandAllButton(page)).toHaveText('👉') + + await expandAllButton(page).click() + + await expect(expandAllButton(page)).toHaveText('👇') + // Expanding everything is stored as `true` rather than a map of row ids. + await expect.poll(async () => (await readState(page)).expanded).toBe(true) + // The first page is now a root followed by its own descendants. + await expect(rowIndent(page, 1)).toHaveCSS('padding-left', '28px') + await expect(rowIndent(page, 2)).toHaveCSS('padding-left', '56px') + + await expandAllButton(page).click() + + await expect(expandAllButton(page)).toHaveText('👉') + await expect(rowIndent(page, 1)).toHaveCSS('padding-left', '0px') + + expect(errors).toEqual([]) +}) + +test('marks rows without children as leaves', async ({ page }) => { + const errors = await openExample(page) + + await expandAllButton(page).click() + await expect(expandAllButton(page)).toHaveText('👇') + + // The third level has no children of its own, so it renders a leaf marker + // instead of an expander. + await expect(page.locator('tbody').getByText('🔵').first()).toBeVisible() + + expect(errors).toEqual([]) +}) + +test('selects sub rows along with their parent', async ({ page }) => { + const errors = await openExample(page) + + await rowExpander(page, 0).click() + await expect(rowExpander(page, 0)).toHaveText('👇') + + await rowCheckbox(page, 0).check() + + // Selecting a parent cascades to its whole subtree, so one click selects far + // more than one row. + await expect.poll(() => countSelected(page)).toBeGreaterThan(CHILDREN_PER_ROW) + await expect(rowCheckbox(page, 1)).toBeChecked() + + await rowCheckbox(page, 0).uncheck() + + await expect.poll(() => countSelected(page)).toBe(0) + await expect(rowCheckbox(page, 1)).not.toBeChecked() + + expect(errors).toEqual([]) +}) diff --git a/examples/octane/expanding/tsconfig.json b/examples/octane/expanding/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/expanding/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/expanding/vite.config.ts b/examples/octane/expanding/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/expanding/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/filters-faceted-bucketed/index.html b/examples/octane/filters-faceted-bucketed/index.html new file mode 100644 index 0000000000..bf859cb95f --- /dev/null +++ b/examples/octane/filters-faceted-bucketed/index.html @@ -0,0 +1,13 @@ + + + + + + + TanStack Preact Table - Bucketed Faceted Filters + + +
+ + + diff --git a/examples/octane/filters-faceted-bucketed/package.json b/examples/octane/filters-faceted-bucketed/package.json new file mode 100644 index 0000000000..031c57e8f8 --- /dev/null +++ b/examples/octane/filters-faceted-bucketed/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-filters-faceted-bucketed", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/filters-faceted-bucketed/src/index.css b/examples/octane/filters-faceted-bucketed/src/index.css new file mode 100644 index 0000000000..9175876031 --- /dev/null +++ b/examples/octane/filters-faceted-bucketed/src/index.css @@ -0,0 +1,104 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; + text-align: left; + vertical-align: top; +} + +td { + padding: 2px 4px; +} + +.demo-root { + padding: 0.5rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.controls, +.pagination, +.inline-controls { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.demo-button { + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.table-container { + overflow-x: auto; +} + +.header-label { + margin-bottom: 0.25rem; +} + +.text-filter { + width: 8rem; +} + +.facet-options { + display: flex; + flex-direction: column; + gap: 0.125rem; + margin: 0; + border: 0; + padding: 0; + font-weight: normal; +} + +.facet-options label { + display: flex; + align-items: center; + gap: 0.25rem; + white-space: nowrap; +} + +.count { + margin-left: auto; + color: gray; +} + +.pagination { + margin-top: 0.5rem; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; +} diff --git a/examples/octane/filters-faceted-bucketed/src/main.tsrx b/examples/octane/filters-faceted-bucketed/src/main.tsrx new file mode 100644 index 0000000000..995a151ac4 --- /dev/null +++ b/examples/octane/filters-faceted-bucketed/src/main.tsrx @@ -0,0 +1,400 @@ +import { createRoot } from 'octane' +import { useState } from 'octane' + +import { + columnFacetingFeature, + columnFilteringFeature, + constructFilterFn, + createColumnHelper, + createFacetedRowModel, + createFacetedUniqueValues, + createFilteredRowModel, + createPaginatedRowModel, + filterFn_includesString, + metaHelper, + rowPaginationFeature, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import './index.css' +import { dataReferenceDate, makeData } from './makeData' +import type { Column, OctaneTable } from '@tanstack/octane-table' +import type { Account } from './makeData' + +type FacetKey = string + +type Bucket = { + value: FacetKey + label: string + test: (value: TValue) => boolean +} + +type FacetOption = Pick, 'value' | 'label'> + +interface MyColumnMeta { + filterVariant?: 'text' | 'facets' + facetOptions?: ReadonlyArray +} + +// Relative date buckets need a stable reference time. An application that stays +// open across midnight should update this value and rebuild its data/columns. +const now = dataReferenceDate +const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()) +const startOfYesterday = new Date(startOfToday) +startOfYesterday.setDate(startOfYesterday.getDate() - 1) +const startOfWeek = new Date(startOfToday) +const daysSinceMonday = (startOfWeek.getDay() + 6) % 7 +startOfWeek.setDate(startOfWeek.getDate() - daysSinceMonday) +const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1) + +const lastLoginBuckets = [ + { + value: 'today', + label: 'Today', + test: (value: Date) => value >= startOfToday, + }, + { + value: 'yesterday', + label: 'Yesterday', + test: (value: Date) => value >= startOfYesterday, + }, + { + value: 'this-week', + label: 'This week', + test: (value: Date) => value >= startOfWeek, + }, + { + value: 'this-month', + label: 'This month', + test: (value: Date) => value >= startOfMonth, + }, + { + value: 'older', + label: 'Older', + test: () => true, + }, +] satisfies ReadonlyArray> + +const GB = 1024 ** 3 +const storageBuckets = [ + { + value: 'under-1-gb', + label: '< 1 GB', + test: (value: number) => value < GB, + }, + { + value: '1-to-10-gb', + label: '1–10 GB', + test: (value: number) => value < 10 * GB, + }, + { + value: '10-to-100-gb', + label: '10–100 GB', + test: (value: number) => value < 100 * GB, + }, + { + value: '100-gb-plus', + label: '100+ GB', + test: () => true, + }, +] satisfies ReadonlyArray> + +function getBucket( + value: TValue, + buckets: ReadonlyArray>, +): FacetKey { + const bucket = buckets.find((candidate) => candidate.test(value)) + + if (!bucket) { + throw new Error(`No facet bucket matched ${String(value)}`) + } + + return bucket.value +} + +// Faceting and filtering deliberately share the same bucket definitions. The +// column keeps its raw value for rendering, sorting, and all other operations. +function createBucketFilter(buckets: ReadonlyArray>) { + return constructFilterFn({ + resolveDataValue: (value) => getBucket(value as TValue, buckets), + filter: (bucketValue, selectedBuckets: ReadonlyArray) => + selectedBuckets.includes(bucketValue), + autoRemove: (selectedBuckets: ReadonlyArray) => + !selectedBuckets.length, + }) +} + +const features = tableFeatures({ + columnFacetingFeature, + columnFilteringFeature, + rowPaginationFeature, + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + filterFns: { + includesString: filterFn_includesString, + }, + columnMeta: metaHelper(), +}) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('name', { + header: 'Account', + filterFn: 'includesString', + meta: { filterVariant: 'text' }, + }), + columnHelper.accessor('lastLogin', { + header: 'Last login', + cell: (info) => info.getValue().toLocaleString(), + getUniqueValues: (row) => [getBucket(row.lastLogin, lastLoginBuckets)], + filterFn: createBucketFilter(lastLoginBuckets), + meta: { + filterVariant: 'facets', + facetOptions: lastLoginBuckets, + }, + }), + columnHelper.accessor('storageBytes', { + header: 'Storage', + cell: (info) => formatBytes(info.getValue()), + getUniqueValues: (row) => [getBucket(row.storageBytes, storageBuckets)], + filterFn: createBucketFilter(storageBuckets), + meta: { + filterVariant: 'facets', + facetOptions: storageBuckets, + }, + }), + columnHelper.accessor('files', { + header: 'Files', + enableColumnFilter: false, + cell: (info) => info.getValue().toLocaleString(), + }), +]) + +function App() @{ + const [data, setData] = useState>(() => makeData(5_000)) + const refreshData = () => setData(makeData(5_000)) + const stressTest = () => setData(makeData(1_000_000)) + + const table = useTable( + { + features, + columns, + data, + debugTable: true, + debugHeaders: true, + debugColumns: false, + }, + (state) => state, // default selector + ) + +
+

Bucketed faceted filters

+

+ Bucket continuous values without a hidden derived column. Select more + than one bucket to match either value; counts react to filters on the + other columns. +

+ +
+ + + +
+
+ +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getAllCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( + <> +
+ +
+ {header.column.getCanFilter() ? ( + + ) : null} + + )} +
+ +
+
+ +
+ + + + + +
Page
+ + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + { + const page = event.currentTarget.value + ? Number(event.currentTarget.value) - 1 + : 0 + table.setPageIndex(page) + }} + className="page-size-input" + /> + + +
+
+ {String(table.getPrePaginatedRowModel().rows.length.toLocaleString())} Rows +
+
+ +
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +function Filter({ + column, + table, +}: { + column: Column + table: OctaneTable +}) @{ + const { filterVariant, facetOptions } = column.columnDef.meta ?? {} + + state.columnFilters}> + {() => + filterVariant === 'facets' && facetOptions ? ( + } + counts={column.getFacetedUniqueValues()} + /> + ) : ( + + column.setFilterValue(event.currentTarget.value) + } + placeholder="Search…" + /> + ) + } + +} + +function FacetFilter({ + column, + options, + selected, + counts, +}: { + column: Column + options: ReadonlyArray + selected: Array + counts: Map +}) @{ + const toggleBucket = (value: FacetKey) => { + column.setFilterValue( + selected.includes(value) + ? selected.filter((selectedValue) => selectedValue !== value) + : [...selected, value], + ) + } + +
+ Filter {column.id} + {options.map((option) => ( + + ))} +
+} + +function formatBytes(value: number) { + if (value < GB) return `${(value / 1024 ** 2).toFixed(0)} MB` + return `${(value / GB).toFixed(1)} GB` +} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/filters-faceted-bucketed/src/makeData.ts b/examples/octane/filters-faceted-bucketed/src/makeData.ts new file mode 100644 index 0000000000..2acc2d0d76 --- /dev/null +++ b/examples/octane/filters-faceted-bucketed/src/makeData.ts @@ -0,0 +1,28 @@ +import { faker } from '@faker-js/faker' + +export type Account = { + name: string + lastLogin: Date + storageBytes: number + files: number +} + +export const dataReferenceDate = new Date() + +const newAccount = (): Account => { + const ageInDays = faker.number.float({ min: 0, max: 1 }) ** 2 * 400 + const storageInGb = 0.05 * 10 ** faker.number.float({ min: 0, max: 4 }) + + return { + name: faker.person.fullName(), + lastLogin: new Date( + dataReferenceDate.getTime() - ageInDays * 24 * 60 * 60 * 1000, + ), + storageBytes: storageInGb * 1024 ** 3, + files: faker.number.int({ max: 50_000 }), + } +} + +export function makeData(len: number): Array { + return Array.from({ length: len }, newAccount) +} diff --git a/examples/octane/filters-faceted-bucketed/src/vite-env.d.ts b/examples/octane/filters-faceted-bucketed/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/filters-faceted-bucketed/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/filters-faceted-bucketed/tests/e2e/smoke.spec.ts b/examples/octane/filters-faceted-bucketed/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..5c72b1160f --- /dev/null +++ b/examples/octane/filters-faceted-bucketed/tests/e2e/smoke.spec.ts @@ -0,0 +1,128 @@ +import { expect, test } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' +import path from 'node:path' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return { errors, server } +} + +function getTable(page: Page) { + return page + .locator('table:visible, .divTable:visible') + .filter({ has: page.locator('thead th, .thead .th') }) + .filter({ has: page.locator('tbody tr, .tbody .tr') }) + .first() +} + +function getHeaderCells(table: Locator) { + return table.locator('thead th, .thead .th') +} + +function getBodyRows(table: Locator) { + return table.locator('tbody tr, .tbody .tr') +} + +async function getFirstBodyRowData(table: Locator) { + const row = getBodyRows(table).first() + const firstInput = row + .locator('input:not([type="checkbox"]):not([type="radio"])') + .first() + + if ((await firstInput.count()) > 0 && (await firstInput.isVisible())) { + return firstInput.inputValue() + } + + const text = await row.textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const { errors, server } = await openExample(page) + + try { + const table = getTable(page) + const bodyRows = getBodyRows(table) + + await expect(table).toBeVisible() + await expect(getHeaderCells(table).first()).toBeVisible() + await expect(bodyRows.first()).toBeVisible() + + const regenerateButton = page + .getByRole('button', { name: /^Regenerate Data$/i }) + .first() + + if ((await regenerateButton.count()) > 0) { + await expect(regenerateButton).toBeVisible() + + const firstRowBefore = await getFirstBodyRowData(table) + + await regenerateButton.click() + + await expect + .poll(() => getFirstBodyRowData(table)) + .not.toBe(firstRowBefore) + await expect(bodyRows.first()).toBeVisible() + } + + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) + +test('filters by a facet bucket', async ({ page }) => { + const { errors, server } = await openExample(page) + + try { + const table = getTable(page) + const todayCheckbox = page.getByRole('checkbox', { name: /Today/ }) + const todayLabel = page.locator('label').filter({ has: todayCheckbox }) + const todayCount = Number( + (await todayLabel.locator('.count').textContent())!, + ) + + await expect(getBodyRows(table)).toHaveCount(10) + await todayCheckbox.check() + + await expect(todayCheckbox).toBeChecked() + await expect(getBodyRows(table)).toHaveCount(Math.min(todayCount, 10)) + await expect + .poll(async () => { + const storageCounts = await page + .locator('.facet-options') + .nth(1) + .locator('.count') + .allTextContents() + + return storageCounts.reduce((sum, count) => sum + Number(count), 0) + }) + .toBe(todayCount) + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/filters-faceted-bucketed/tsconfig.json b/examples/octane/filters-faceted-bucketed/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/filters-faceted-bucketed/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/filters-faceted-bucketed/vite.config.ts b/examples/octane/filters-faceted-bucketed/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/filters-faceted-bucketed/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/filters-faceted/index.html b/examples/octane/filters-faceted/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/filters-faceted/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/filters-faceted/package.json b/examples/octane/filters-faceted/package.json new file mode 100644 index 0000000000..81858c26cc --- /dev/null +++ b/examples/octane/filters-faceted/package.json @@ -0,0 +1,24 @@ +{ + "name": "@tanstack/octane-table-example-filters-faceted", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/match-sorter-utils": "^9.0.0-beta.53", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/filters-faceted/src/index.css b/examples/octane/filters-faceted/src/index.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/octane/filters-faceted/src/index.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/filters-faceted/src/main.tsrx b/examples/octane/filters-faceted/src/main.tsrx new file mode 100644 index 0000000000..6b10158266 --- /dev/null +++ b/examples/octane/filters-faceted/src/main.tsrx @@ -0,0 +1,376 @@ +import { createRoot } from 'octane' +import { useEffect, useMemo, useRef, useState } from 'octane' +import './index.css' +import { + columnFacetingFeature, + columnFilteringFeature, + createColumnHelper, + createFacetedMinMaxValues, + createFacetedRowModel, + createFacetedUniqueValues, + createFilteredRowModel, + createPaginatedRowModel, + filterFn_equalsString, + filterFn_inNumberRange, + filterFn_includesString, + metaHelper, + rowPaginationFeature, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Column } from '@tanstack/octane-table' +import type { Person } from './makeData' + +// allows us to define custom properties for our columns +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' +} + +const features = tableFeatures({ + columnFacetingFeature, + columnFilteringFeature, + rowPaginationFeature, + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + facetedRowModel: createFacetedRowModel(), + facetedMinMaxValues: createFacetedMinMaxValues(), + facetedUniqueValues: createFacetedUniqueValues(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + equalsString: filterFn_equalsString, + }, + columnMeta: metaHelper(), +}) + +const columnHelper = createColumnHelper() + +function App() @{ + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + }), + columnHelper.accessor('age', { + header: () => 'Age', + meta: { + filterVariant: 'range', + }, + }), + columnHelper.accessor('visits', { + header: () => Visits, + meta: { + filterVariant: 'range', + }, + }), + columnHelper.accessor('status', { + header: 'Status', + filterFn: 'equalsString', // filterFn string to pick from filterFns + meta: { + filterVariant: 'select', + }, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + meta: { + filterVariant: 'range', + }, + filterFn: filterFn_inNumberRange, // or just reference static filterFn from import + // you could also write your own custom filter function here + }), + ]), + [], + ) + + const [data, setData] = useState>(() => makeData(5_000)) + const refreshData = () => setData((_old) => makeData(5_000)) + const stressTest = () => setData((_old) => makeData(1_000_000)) + + const table = useTable( + { + features, + columns, + data, + // Column faceting has no table-level options; configure its row-model factories in `features`. + // initialState: { columnFilters: [{ id: 'firstName', value: 'Jane' }] }, // set filters once + // atoms: { columnFilters: columnFiltersAtom }, // preferred: own column filters with an external atom + // state: { columnFilters }, // classic controlled state; pair with onColumnFiltersChange + // onColumnFiltersChange: setColumnFilters, + // enableFilters: false, // disable all column and global filtering; default true + // enableColumnFilters: false, // disable per-column filters; default true + // filterFromLeafRows: true, // keep parents whose descendants match; default filters from parents down + // maxLeafRowFilterDepth: 1, // only filter through this nested-row depth; default 100 + // manualFiltering: true, // pass data that is already filtered, for example from a server + debugTable: true, + debugHeaders: true, + debugColumns: false, + }, + (state) => state, // default selector + ) + +
+
+ + +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + return ( + + {row.getAllCells().map((cell) => { + return ( + + ) + })} + + ) + })} + +
+ {header.isPlaceholder ? null : ( + <> +
+ +
+ {header.column.getCanFilter() ? ( +
+ +
+ ) : null} + + )} +
+ +
+
+
+ + + + + +
Page
+ + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + { + const page = (e.target as HTMLInputElement).value + ? Number((e.target as HTMLInputElement).value) - 1 + : 0 + table.setPageIndex(page) + }} + className="page-size-input" + /> + + +
+
+ {String(table.getPrePaginatedRowModel().rows.length.toLocaleString())} Rows +
+
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +function Filter({ column }: { column: Column }) { + const { filterVariant } = column.columnDef.meta ?? {} + + const columnFilterValue = column.getFilterValue() + + const minMaxValues = column.getFacetedMinMaxValues() + + const sortedUniqueValues = useMemo( + () => + filterVariant === 'range' + ? [] + : Array.from(column.getFacetedUniqueValues().keys()) + .sort() + .slice(0, 5000), + [column.getFacetedUniqueValues(), filterVariant], + ) + + return filterVariant === 'range' ? ( +
+
+ + column.setFilterValue((old: [number, number] | undefined) => [ + value, + old?.[1], + ]) + } + placeholder={`Min ${ + minMaxValues?.[0] !== undefined ? `(${minMaxValues[0]})` : '' + }`} + className="filter-input" + /> + + column.setFilterValue((old: [number, number] | undefined) => [ + old?.[0], + value, + ]) + } + placeholder={`Max ${minMaxValues?.[1] ? `(${minMaxValues[1]})` : ''}`} + className="filter-input" + /> +
+
+
+ ) : filterVariant === 'select' ? ( + + ) : ( + <> + {/* Autocomplete suggestions from faceted values feature */} + + {sortedUniqueValues.map((value: any) => ( + + column.setFilterValue(value)} + placeholder={`Search... (${column.getFacetedUniqueValues().size})`} + className="filter-select" + list={column.id + 'list'} + /> +
+ + ) +} + +function useDebouncedCallback( + callback: (value: T) => void, + { wait }: { wait: number }, +) { + const callbackRef = useRef(callback) + const timeoutRef = useRef(undefined) + callbackRef.current = callback + + useEffect(() => () => window.clearTimeout(timeoutRef.current), []) + + return (value: T) => { + window.clearTimeout(timeoutRef.current) + timeoutRef.current = window.setTimeout(() => callbackRef.current(value), wait) + } +} + +// A typical debounced input Octane component +function DebouncedInput({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Record) @{ + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + const debouncedOnChange = useDebouncedCallback(onChange, { wait: debounce }) + + { + const nextValue = (e.target as HTMLInputElement).value + setValue(nextValue) + debouncedOnChange(nextValue) + }} + /> +} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/filters-faceted/src/makeData.ts b/examples/octane/filters-faceted/src/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/octane/filters-faceted/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/filters-faceted/src/vite-env.d.ts b/examples/octane/filters-faceted/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/filters-faceted/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/filters-faceted/tests/e2e/smoke.spec.ts b/examples/octane/filters-faceted/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..868e818817 --- /dev/null +++ b/examples/octane/filters-faceted/tests/e2e/smoke.spec.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' +import path from 'node:path' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return { errors, server } +} + +function getTable(page: Page) { + return page + .locator('table:visible, .divTable:visible') + .filter({ has: page.locator('thead th, .thead .th') }) + .filter({ has: page.locator('tbody tr, .tbody .tr') }) + .first() +} + +function getHeaderCells(table: Locator) { + return table.locator('thead th, .thead .th') +} + +function getBodyRows(table: Locator) { + return table.locator('tbody tr, .tbody .tr') +} + +async function getFirstBodyRowData(table: Locator) { + const row = getBodyRows(table).first() + const firstInput = row + .locator('input:not([type="checkbox"]):not([type="radio"])') + .first() + + if ((await firstInput.count()) > 0 && (await firstInput.isVisible())) { + return firstInput.inputValue() + } + + const text = await row.textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const { errors, server } = await openExample(page) + + try { + const table = getTable(page) + const bodyRows = getBodyRows(table) + + await expect(table).toBeVisible() + await expect(getHeaderCells(table).first()).toBeVisible() + await expect(bodyRows.first()).toBeVisible() + + const regenerateButton = page + .getByRole('button', { name: /^Regenerate Data$/i }) + .first() + + if ((await regenerateButton.count()) > 0) { + await expect(regenerateButton).toBeVisible() + + const firstRowBefore = await getFirstBodyRowData(table) + + await regenerateButton.click() + + await expect + .poll(() => getFirstBodyRowData(table)) + .not.toBe(firstRowBefore) + await expect(bodyRows.first()).toBeVisible() + } + + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/filters-faceted/tsconfig.json b/examples/octane/filters-faceted/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/filters-faceted/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/filters-faceted/vite.config.ts b/examples/octane/filters-faceted/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/filters-faceted/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/filters-fuzzy/index.html b/examples/octane/filters-fuzzy/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/filters-fuzzy/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/filters-fuzzy/package.json b/examples/octane/filters-fuzzy/package.json new file mode 100644 index 0000000000..a411c4a0e9 --- /dev/null +++ b/examples/octane/filters-fuzzy/package.json @@ -0,0 +1,24 @@ +{ + "name": "@tanstack/octane-table-example-filters-fuzzy", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/match-sorter-utils": "^9.0.0-beta.53", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/filters-fuzzy/src/index.css b/examples/octane/filters-fuzzy/src/index.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/octane/filters-fuzzy/src/index.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/filters-fuzzy/src/main.tsrx b/examples/octane/filters-fuzzy/src/main.tsrx new file mode 100644 index 0000000000..eabe2cc4dc --- /dev/null +++ b/examples/octane/filters-fuzzy/src/main.tsrx @@ -0,0 +1,371 @@ +import { createRoot } from 'octane' +import { useEffect, useMemo, useRef, useState } from 'octane' +import './index.css' +import { + columnFilteringFeature, + createColumnHelper, + createFilteredRowModel, + createPaginatedRowModel, + createSortedRowModel, + filterFn_equalsString, + filterFn_includesString, + filterFn_includesStringSensitive, + globalFilteringFeature, + metaHelper, + rowPaginationFeature, + rowSortingFeature, + sortFn_alphanumeric, + sortFn_text, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { compareItems, rankItem } from '@tanstack/match-sorter-utils' +import { makeData } from './makeData' +import type { Person } from './makeData' +import type { + Column, + FilterFn, + SortFn, + TableFeatures, +} from '@tanstack/octane-table' + +// A TanStack fork of Kent C. Dodds' match-sorter library that provides ranking information +import type { RankingInfo } from '@tanstack/match-sorter-utils' + +// The filter meta that the fuzzy filter attaches to rows, declared per-table +// via the `filterMeta` slot below. No declaration merging needed! +interface FuzzyFilterMeta { + itemRank?: RankingInfo +} + +// Broad features type for writing the custom fns below before the `features` +// object exists, with the filter meta type plugged in +type FuzzyFeatures = TableFeatures & { filterMeta: FuzzyFilterMeta } + +// Define a custom fuzzy filter function that will apply ranking info to rows (using match-sorter utils) +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 +} + +// Define a custom fuzzy sort function that will sort by rank if the row has ranking information +const fuzzySort: SortFn = (rowA, rowB, columnId) => { + let dir = 0 + + // Only sort by rank if the column has ranking information + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (rowA.columnFiltersMeta[columnId]) { + dir = compareItems( + rowA.columnFiltersMeta[columnId].itemRank!, + rowB.columnFiltersMeta[columnId].itemRank!, + ) + } + + // Provide an alphanumeric fallback for when the item ranks are equal + return dir === 0 ? sortFn_alphanumeric(rowA, rowB, columnId) : dir +} + +const features = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + rowSortingFeature, + rowPaginationFeature, + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortedRowModel: createSortedRowModel(), + filterFns: { + equalsString: filterFn_equalsString, + includesString: filterFn_includesString, + includesStringSensitive: filterFn_includesStringSensitive, + fuzzy: fuzzyFilter, + }, + sortFns: { + alphanumeric: sortFn_alphanumeric, + text: sortFn_text, + fuzzy: fuzzySort, + }, + filterMeta: metaHelper(), +}) + +const columnHelper = createColumnHelper() + +function App() @{ + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('id', { + filterFn: 'equalsString', // note: normal non-fuzzy filter column - exact match required + }), + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + filterFn: 'includesStringSensitive', // note: normal non-fuzzy filter column - case sensitive + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + filterFn: 'includesString', // note: normal non-fuzzy filter column - case insensitive + }), + columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, { + id: 'fullName', + header: 'Full Name', + cell: (info) => info.getValue(), + filterFn: 'fuzzy', // using our custom fuzzy filter function + // filterFn: fuzzyFilter, //or just define with the function + sortFn: 'fuzzy', // sort by fuzzy rank (falls back to alphanumeric) + }), + ]), + [], + ) + + const [data, setData] = useState>(() => makeData(5_000)) + const refreshData = () => setData((_old) => makeData(5_000)) + const stressTest = () => setData((_old) => makeData(1_000_000)) + + const table = useTable( + { + features, + columns, + data, + globalFilterFn: 'fuzzy', // apply fuzzy filter to the global filter (most common use case for fuzzy filter) + // initialState: { columnFilters: [{ id: 'firstName', value: 'Jane' }], globalFilter: 'Jane' }, // set filters once + // atoms: { columnFilters: columnFiltersAtom, globalFilter: globalFilterAtom }, // preferred external ownership + // state: { columnFilters, globalFilter }, // classic controlled state; pair with the callbacks below + // onColumnFiltersChange: setColumnFilters, + // onGlobalFilterChange: setGlobalFilter, + // enableFilters: false, // disable all column and global filtering; default true + // enableColumnFilters: false, // disable per-column filters; default true + // filterFromLeafRows: true, // keep parents whose descendants match; default filters from parents down + // maxLeafRowFilterDepth: 1, // only filter through this nested-row depth; default 100 + // manualFiltering: true, // pass data that is already filtered, for example from a server + // enableGlobalFilter: false, // disable the global filter input; default true + // getColumnCanGlobalFilter: column => column.id !== 'status', // opt a column out of global filtering + debugTable: true, + debugHeaders: true, + debugColumns: false, + }, + (state) => state, // default selector + ) + + // apply the fuzzy sort if the fullName column is being filtered + useEffect(() => { + if (table.state.columnFilters[0]?.id === 'fullName') { + if (table.state.sorting[0]?.id !== 'fullName') { + table.setSorting([{ id: 'fullName', desc: false }]) + } + } + }, [table.state.columnFilters[0]?.id]) + +
+
+ + +
+
+ table.setGlobalFilter(String(value))} + className="summary-panel" + placeholder="Search all columns..." + /> +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + return ( + + {row.getAllCells().map((cell) => { + return ( + + ) + })} + + ) + })} + +
+ {header.isPlaceholder ? null : ( + <> +
+ + {{ + asc: ' 🔼', + desc: ' 🔽', + }[header.column.getIsSorted() as string] ?? null} +
+ {header.column.getCanFilter() ? ( +
+ +
+ ) : null} + + )} +
+ +
+
+
+ + + + + +
Page
+ + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + { + const page = (e.target as HTMLInputElement).value + ? Number((e.target as HTMLInputElement).value) - 1 + : 0 + table.setPageIndex(page) + }} + className="page-size-input" + /> + + +
+
+ {String(table.getPrePaginatedRowModel().rows.length.toLocaleString())} Rows +
+
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +function Filter({ column }: { column: Column }) @{ + const columnFilterValue = column.getFilterValue() + + column.setFilterValue(value)} + placeholder={`Search...`} + className="filter-select" + /> +} + +function useDebouncedCallback( + callback: (value: T) => void, + { wait }: { wait: number }, +) { + const callbackRef = useRef(callback) + const timeoutRef = useRef(undefined) + callbackRef.current = callback + + useEffect(() => () => window.clearTimeout(timeoutRef.current), []) + + return (value: T) => { + window.clearTimeout(timeoutRef.current) + timeoutRef.current = window.setTimeout(() => callbackRef.current(value), wait) + } +} + +// A typical debounced input Octane component +function DebouncedInput({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Record) @{ + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + const debouncedOnChange = useDebouncedCallback(onChange, { wait: debounce }) + + { + const nextValue = (e.target as HTMLInputElement).value + setValue(nextValue) + debouncedOnChange(nextValue) + }} + /> +} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/filters-fuzzy/src/makeData.ts b/examples/octane/filters-fuzzy/src/makeData.ts new file mode 100644 index 0000000000..8d13e339f5 --- /dev/null +++ b/examples/octane/filters-fuzzy/src/makeData.ts @@ -0,0 +1,50 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + id: number + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (num: number): Person => { + return { + id: num, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((index): Person => { + return { + ...newPerson(index), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/filters-fuzzy/src/vite-env.d.ts b/examples/octane/filters-fuzzy/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/filters-fuzzy/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/filters-fuzzy/tests/e2e/smoke.spec.ts b/examples/octane/filters-fuzzy/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..868e818817 --- /dev/null +++ b/examples/octane/filters-fuzzy/tests/e2e/smoke.spec.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' +import path from 'node:path' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return { errors, server } +} + +function getTable(page: Page) { + return page + .locator('table:visible, .divTable:visible') + .filter({ has: page.locator('thead th, .thead .th') }) + .filter({ has: page.locator('tbody tr, .tbody .tr') }) + .first() +} + +function getHeaderCells(table: Locator) { + return table.locator('thead th, .thead .th') +} + +function getBodyRows(table: Locator) { + return table.locator('tbody tr, .tbody .tr') +} + +async function getFirstBodyRowData(table: Locator) { + const row = getBodyRows(table).first() + const firstInput = row + .locator('input:not([type="checkbox"]):not([type="radio"])') + .first() + + if ((await firstInput.count()) > 0 && (await firstInput.isVisible())) { + return firstInput.inputValue() + } + + const text = await row.textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const { errors, server } = await openExample(page) + + try { + const table = getTable(page) + const bodyRows = getBodyRows(table) + + await expect(table).toBeVisible() + await expect(getHeaderCells(table).first()).toBeVisible() + await expect(bodyRows.first()).toBeVisible() + + const regenerateButton = page + .getByRole('button', { name: /^Regenerate Data$/i }) + .first() + + if ((await regenerateButton.count()) > 0) { + await expect(regenerateButton).toBeVisible() + + const firstRowBefore = await getFirstBodyRowData(table) + + await regenerateButton.click() + + await expect + .poll(() => getFirstBodyRowData(table)) + .not.toBe(firstRowBefore) + await expect(bodyRows.first()).toBeVisible() + } + + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/filters-fuzzy/tsconfig.json b/examples/octane/filters-fuzzy/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/filters-fuzzy/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/filters-fuzzy/vite.config.ts b/examples/octane/filters-fuzzy/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/filters-fuzzy/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/filters/index.html b/examples/octane/filters/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/filters/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/filters/package.json b/examples/octane/filters/package.json new file mode 100644 index 0000000000..ec2d5b9467 --- /dev/null +++ b/examples/octane/filters/package.json @@ -0,0 +1,24 @@ +{ + "name": "@tanstack/octane-table-example-filters", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/match-sorter-utils": "^9.0.0-beta.53", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/filters/src/index.css b/examples/octane/filters/src/index.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/octane/filters/src/index.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/filters/src/main.tsrx b/examples/octane/filters/src/main.tsrx new file mode 100644 index 0000000000..a77c63b786 --- /dev/null +++ b/examples/octane/filters/src/main.tsrx @@ -0,0 +1,353 @@ +import { createRoot } from 'octane' +import { useEffect, useMemo, useRef, useState } from 'octane' +import './index.css' +import { + columnFilteringFeature, + createColumnHelper, + createFilteredRowModel, + createPaginatedRowModel, + filterFn_equalsString, + filterFn_inNumberRange, + filterFn_includesString, + metaHelper, + rowPaginationFeature, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Column } from '@tanstack/octane-table' +import type { Person } from './makeData' + +// allows us to define custom properties for our columns +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' +} + +const features = tableFeatures({ + columnFilteringFeature, + rowPaginationFeature, + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + equalsString: filterFn_equalsString, + }, + columnMeta: metaHelper(), +}) + +const columnHelper = createColumnHelper() + +function App() @{ + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.display({ + id: 'rowNumber', + header: '#', + cell: ({ row }) => row.getDisplayIndex() + 1, + }), + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + }), + columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, { + id: 'fullName', + header: 'Full Name', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('age', { + header: () => 'Age', + meta: { + filterVariant: 'range', + }, + }), + columnHelper.accessor('visits', { + header: () => Visits, + meta: { + filterVariant: 'range', + }, + }), + columnHelper.accessor('status', { + header: 'Status', + filterFn: 'equalsString', // filterFn string to pick from filterFns + meta: { + filterVariant: 'select', + }, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + meta: { + filterVariant: 'range', + }, + filterFn: filterFn_inNumberRange, // or just reference static filterFn from import + // you could also write your own custom filter function here + }), + ]), + [], + ) + + const [data, setData] = useState>(() => makeData(5_000)) + const refreshData = () => setData((_old) => makeData(5_000)) + const stressTest = () => setData((_old) => makeData(1_000_000)) + + const table = useTable( + { + features, + columns, + data, + // initialState: { columnFilters: [{ id: 'firstName', value: 'Jane' }] }, // set filters once + // atoms: { columnFilters: columnFiltersAtom }, // preferred: own column filters with an external atom + // state: { columnFilters }, // classic controlled state; pair with onColumnFiltersChange + // onColumnFiltersChange: setColumnFilters, + // enableFilters: false, // disable all column and global filtering; default true + // enableColumnFilters: false, // disable per-column filters; default true + // filterFromLeafRows: true, // keep parents whose descendants match; default filters from parents down + // maxLeafRowFilterDepth: 1, // only filter through this nested-row depth; default 100 + // manualFiltering: true, // pass data that is already filtered, for example from a server + debugTable: true, + debugColumns: true, + }, + (state) => state, // default selector + ) + +
+
+ + +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + return ( + + {row.getAllCells().map((cell) => { + return ( + + ) + })} + + ) + })} + +
+ {header.isPlaceholder ? null : ( + <> +
+ +
+ {header.column.getCanFilter() ? ( +
+ +
+ ) : null} + + )} +
+ +
+
+
+ + + + + +
Page
+ + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + { + const page = (e.target as HTMLInputElement).value + ? Number((e.target as HTMLInputElement).value) - 1 + : 0 + table.setPageIndex(page) + }} + className="page-size-input" + /> + + +
+
+ {String(table.getPrePaginatedRowModel().rows.length.toLocaleString())} Rows +
+
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +function Filter({ + column, +}: { + column: Column +}) { + const columnFilterValue = column.getFilterValue() + const { filterVariant } = column.columnDef.meta ?? {} + + return filterVariant === 'range' ? ( +
+
+ {/* See faceted column filters example for min max values functionality */} + + column.setFilterValue((old: [number, number] | undefined) => [ + value, + old?.[1], + ]) + } + placeholder={`Min`} + className="filter-input" + /> + + column.setFilterValue((old: [number, number] | undefined) => [ + old?.[0], + value, + ]) + } + placeholder={`Max`} + className="filter-input" + /> +
+
+
+ ) : filterVariant === 'select' ? ( + + ) : ( + column.setFilterValue(value)} + placeholder={`Search...`} + type="text" + value={(columnFilterValue ?? '') as string} + /> + // See faceted column filters example for datalist search suggestions + ) +} + +function useDebouncedCallback( + callback: (value: T) => void, + { wait }: { wait: number }, +) { + const callbackRef = useRef(callback) + const timeoutRef = useRef(undefined) + callbackRef.current = callback + + useEffect(() => () => window.clearTimeout(timeoutRef.current), []) + + return (value: T) => { + window.clearTimeout(timeoutRef.current) + timeoutRef.current = window.setTimeout(() => callbackRef.current(value), wait) + } +} + +// A typical debounced input Octane component +function DebouncedInput({ + value: initialValue, + onChange, + debounce = 500, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Record) @{ + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + const debouncedOnChange = useDebouncedCallback(onChange, { wait: debounce }) + + { + const nextValue = (e.target as HTMLInputElement).value + setValue(nextValue) + debouncedOnChange(nextValue) + }} + /> +} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/filters/src/makeData.ts b/examples/octane/filters/src/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/octane/filters/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/filters/src/vite-env.d.ts b/examples/octane/filters/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/filters/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/filters/tests/e2e/smoke.spec.ts b/examples/octane/filters/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..ba18aab696 --- /dev/null +++ b/examples/octane/filters/tests/e2e/smoke.spec.ts @@ -0,0 +1,335 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +const TOTAL_ROWS = 5_000 + +/** Header cell index per column, matching the column order in `src/main.tsrx`. */ +const COLUMN = { + rowNumber: 0, + firstName: 1, + lastName: 2, + fullName: 3, + age: 4, + visits: 5, + status: 6, + progress: 7, +} as const + +const STATUSES = ['complicated', 'relationship', 'single'] + +let server: Awaited> | undefined + +test.beforeAll(async () => { + // One server for the whole file. A cold Vite start costs more than a test. + test.setTimeout(180_000) + server = await startExampleServer(exampleDir) +}) + +test.afterAll(async () => { + await server?.close() +}) + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + if (!server) throw new Error('Example server failed to start') + + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return errors +} + +function headerCell(page: Page, column: keyof typeof COLUMN) { + return page.locator('thead th').nth(COLUMN[column]) +} + +function textFilter(page: Page, column: 'firstName' | 'lastName' | 'fullName') { + return headerCell(page, column).locator('input[type="text"]') +} + +function rangeFilter( + page: Page, + column: 'age' | 'visits' | 'progress', + bound: 'Min' | 'Max', +) { + return headerCell(page, column).getByPlaceholder(bound) +} + +function statusFilter(page: Page) { + return headerCell(page, 'status').locator('select') +} + +/** The pre-paginated count, so it reflects filtering but not the page window. */ +function rowCountLine(page: Page) { + return page.getByTestId('row-count') +} + +async function readFilteredRowCount(page: Page) { + const text = await rowCountLine(page).textContent() + return Number((text ?? '').replace(/\D/g, '')) +} + +/** Row data is random faker output, so `table.state` is the stable oracle. */ +async function readState(page: Page) { + const text = await page.getByTestId('table-state').textContent() + + return JSON.parse(text ?? '{}') as { + columnFilters?: Array<{ id: string; value: unknown }> + pagination?: { pageIndex: number; pageSize: number } + } +} + +async function readColumnFilters(page: Page) { + return (await readState(page)).columnFilters ?? [] +} + +async function expectColumnFilters( + page: Page, + expected: Array<{ id: string; value: unknown }>, +) { + // Every filter control debounces by 500ms, so poll rather than sleeping. + await expect.poll(() => readColumnFilters(page)).toEqual(expected) +} + +async function readBodyColumn(page: Page, column: keyof typeof COLUMN) { + const cells = await page + .locator(`tbody tr td:nth-child(${COLUMN[column] + 1})`) + .allTextContents() + + return cells.map((cell) => cell.trim()) +} + +async function getFirstBodyRowText(page: Page) { + const text = await page.locator('tbody tr').first().textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const errors = await openExample(page) + const table = page.locator('table').first() + + await expect(table).toBeVisible() + await expect(table.locator('thead th')).toHaveCount(8) + await expect(page.locator('tbody tr')).toHaveCount(10) + await expect(rowCountLine(page)).toHaveText('5,000 Rows') + await expectColumnFilters(page, []) + + expect(errors).toEqual([]) +}) + +test('regenerates table data', async ({ page }) => { + const errors = await openExample(page) + const regenerateButton = page.getByRole('button', { + name: /^Regenerate Data$/i, + }) + + await expect(page.locator('tbody tr').first()).toBeVisible() + + const firstRowBefore = await getFirstBodyRowText(page) + + await regenerateButton.click() + + await expect.poll(() => getFirstBodyRowText(page)).not.toBe(firstRowBefore) + await expect(page.locator('tbody tr')).toHaveCount(10) + + expect(errors).toEqual([]) +}) + +test('renders a filter control for every filterable column', async ({ + page, +}) => { + const errors = await openExample(page) + + // Three text columns, three range columns with a Min and a Max, one select. + await expect(page.locator('thead input[type="text"]')).toHaveCount(3) + await expect(page.locator('thead input[type="number"]')).toHaveCount(6) + await expect(page.locator('thead select')).toHaveCount(1) + + await expect(textFilter(page, 'firstName')).toHaveAttribute( + 'placeholder', + 'Search...', + ) + await expect(statusFilter(page).locator('option')).toHaveText([ + 'All', + ...STATUSES, + ]) + + // The row number column has no accessor, so it cannot be filtered. + await expect( + headerCell(page, 'rowNumber').locator('input, select'), + ).toHaveCount(0) + + expect(errors).toEqual([]) +}) + +test('filters by text after the debounce', async ({ page }) => { + const errors = await openExample(page) + + // A fragment of a name that is actually present, so a match is guaranteed + // even though the data is regenerated randomly on every load. + const firstNames = await readBodyColumn(page, 'firstName') + const fragment = (firstNames[0] ?? '').slice(0, 3).toLowerCase() + expect(fragment).not.toBe('') + + await textFilter(page, 'firstName').fill(fragment) + + await expectColumnFilters(page, [{ id: 'firstName', value: fragment }]) + + const filtered = await readFilteredRowCount(page) + expect(filtered).toBeGreaterThan(0) + expect(filtered).toBeLessThan(TOTAL_ROWS) + + for (const name of await readBodyColumn(page, 'firstName')) { + expect(name.toLowerCase()).toContain(fragment) + } + + // Emptying a text filter drops it from state rather than keeping a blank. + await textFilter(page, 'firstName').fill('') + + await expectColumnFilters(page, []) + await expect(rowCountLine(page)).toHaveText('5,000 Rows') + + expect(errors).toEqual([]) +}) + +test('waits for the debounce before applying the filter', async ({ page }) => { + const errors = await openExample(page) + + await textFilter(page, 'firstName').fill('zzz') + + // The 500ms debounce has not elapsed, so state is still untouched. + expect(await readColumnFilters(page)).toEqual([]) + + await expectColumnFilters(page, [{ id: 'firstName', value: 'zzz' }]) + + expect(errors).toEqual([]) +}) + +test('filters a numeric column by range', async ({ page }) => { + const errors = await openExample(page) + + await rangeFilter(page, 'age', 'Min').fill('20') + + // An unset bound serialises as null, leaving the range open ended. + await expectColumnFilters(page, [{ id: 'age', value: ['20', null] }]) + + const minOnly = await readFilteredRowCount(page) + expect(minOnly).toBeLessThan(TOTAL_ROWS) + + for (const age of await readBodyColumn(page, 'age')) { + expect(Number(age)).toBeGreaterThanOrEqual(20) + } + + await rangeFilter(page, 'age', 'Max').fill('25') + + await expectColumnFilters(page, [{ id: 'age', value: ['20', '25'] }]) + expect(await readFilteredRowCount(page)).toBeLessThan(minOnly) + + for (const age of await readBodyColumn(page, 'age')) { + expect(Number(age)).toBeGreaterThanOrEqual(20) + expect(Number(age)).toBeLessThanOrEqual(25) + } + + // Clearing one bound keeps the filter, with that end left empty. + await rangeFilter(page, 'age', 'Min').fill('') + + await expectColumnFilters(page, [{ id: 'age', value: ['', '25'] }]) + + for (const age of await readBodyColumn(page, 'age')) { + expect(Number(age)).toBeLessThanOrEqual(25) + } + + await rangeFilter(page, 'age', 'Max').fill('') + + await expect.poll(() => readFilteredRowCount(page)).toBe(TOTAL_ROWS) + + expect(errors).toEqual([]) +}) + +test('filters by the status select', async ({ page }) => { + const errors = await openExample(page) + const counts: Array = [] + + for (const status of STATUSES) { + await statusFilter(page).selectOption(status) + + await expectColumnFilters(page, [{ id: 'status', value: status }]) + + for (const cell of await readBodyColumn(page, 'status')) { + expect(cell).toBe(status) + } + + counts.push(await readFilteredRowCount(page)) + } + + // Every row has exactly one of the three statuses, so the parts must make up + // the whole. This holds no matter what the random data happens to be. + expect(counts.reduce((total, count) => total + count, 0)).toBe(TOTAL_ROWS) + + // The All option carries an empty value, which removes the filter entirely. + await statusFilter(page).selectOption('') + + await expectColumnFilters(page, []) + await expect(rowCountLine(page)).toHaveText('5,000 Rows') + + expect(errors).toEqual([]) +}) + +test('combines filters and returns to the first page', async ({ page }) => { + const errors = await openExample(page) + + await page.getByRole('button', { name: '>', exact: true }).click() + await page.getByRole('button', { name: '>', exact: true }).click() + await expect + .poll(async () => (await readState(page)).pagination?.pageIndex) + .toBe(2) + + await statusFilter(page).selectOption('single') + + await expectColumnFilters(page, [{ id: 'status', value: 'single' }]) + // `autoResetPageIndex` defaults to true, so filtering rewinds to page one. + await expect + .poll(async () => (await readState(page)).pagination?.pageIndex) + .toBe(0) + + const statusOnly = await readFilteredRowCount(page) + + await rangeFilter(page, 'age', 'Min').fill('20') + + await expectColumnFilters(page, [ + { id: 'status', value: 'single' }, + { id: 'age', value: ['20', null] }, + ]) + // Filters intersect, so adding one can only narrow the result. + expect(await readFilteredRowCount(page)).toBeLessThanOrEqual(statusOnly) + + for (const cell of await readBodyColumn(page, 'status')) { + expect(cell).toBe('single') + } + + for (const age of await readBodyColumn(page, 'age')) { + expect(Number(age)).toBeGreaterThanOrEqual(20) + } + + expect(errors).toEqual([]) +}) diff --git a/examples/octane/filters/tsconfig.json b/examples/octane/filters/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/filters/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/filters/vite.config.ts b/examples/octane/filters/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/filters/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/grouped-aggregation/index.html b/examples/octane/grouped-aggregation/index.html new file mode 100644 index 0000000000..88e0e81100 --- /dev/null +++ b/examples/octane/grouped-aggregation/index.html @@ -0,0 +1,13 @@ + + + + + + + TanStack Preact Table - Grouped Aggregation + + +
+ + + diff --git a/examples/octane/grouped-aggregation/package.json b/examples/octane/grouped-aggregation/package.json new file mode 100644 index 0000000000..040a4978b0 --- /dev/null +++ b/examples/octane/grouped-aggregation/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-grouped-aggregation", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/grouped-aggregation/src/index.css b/examples/octane/grouped-aggregation/src/index.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/octane/grouped-aggregation/src/index.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/grouped-aggregation/src/main.tsrx b/examples/octane/grouped-aggregation/src/main.tsrx new file mode 100644 index 0000000000..24666a44ca --- /dev/null +++ b/examples/octane/grouped-aggregation/src/main.tsrx @@ -0,0 +1,310 @@ +import { useMemo, useState } from 'octane' +import { createRoot } from 'octane' +import './index.css' +import { + rowAggregationFeature, + aggregationFn_mean, + aggregationFn_median, + aggregationFn_sum, + columnFilteringFeature, + columnGroupingFeature, + createExpandedRowModel, + createFilteredRowModel, + createGroupedRowModel, + createPaginatedRowModel, + createSortedRowModel, + createTableHook, + filterFn_inNumberRange, + filterFn_includesString, + rowExpandingFeature, + rowPaginationFeature, + rowSortingFeature, + sortFn_alphanumeric, + sortFn_text, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Person } from './makeData' + +// this example happens to use the createTableHook pattern, but it is not required +const { useAppTable, createAppColumnHelper } = createTableHook({ + features: { + rowAggregationFeature, + columnFilteringFeature, + columnGroupingFeature, + rowExpandingFeature, + rowPaginationFeature, + rowSortingFeature, + expandedRowModel: createExpandedRowModel(), + filteredRowModel: createFilteredRowModel(), + groupedRowModel: createGroupedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortedRowModel: createSortedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, + sortFns: { + alphanumeric: sortFn_alphanumeric, + text: sortFn_text, + }, + aggregationFns: { + mean: aggregationFn_mean, + median: aggregationFn_median, + sum: aggregationFn_sum, + }, + }, +}) + +const columnHelper = createAppColumnHelper() + +function App() @{ + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('firstName', { + header: 'First Name', + footer: 'Grand Total', + cell: (info) => info.getValue(), + /** + * override the value used for row grouping + * (otherwise, defaults to the value derived from accessorKey / accessorFn) + */ + getGroupingValue: (row) => `${row.firstName} ${row.lastName}`, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + header: () => Last Name, + cell: (info) => info.getValue(), + }), + columnHelper.accessor('age', { + header: () => 'Age', + aggregatedCell: ({ getValue }) => + Math.round(getValue() * 100) / 100, + aggregationFn: 'median', + footer: ({ column }) => { + const value = column.getAggregationValue() + return value === undefined ? null : Math.round(value * 100) / 100 + }, + }), + columnHelper.accessor('visits', { + header: () => Visits, + aggregationFn: 'sum', + aggregatedCell: ({ getValue }) => getValue().toLocaleString(), + footer: ({ column }) => + column.getAggregationValue().toLocaleString(), + }), + columnHelper.accessor('status', { + header: 'Status', + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + cell: ({ getValue }) => + Math.round(getValue() * 100) / 100 + '%', + aggregationFn: 'mean', + aggregatedCell: ({ getValue }) => + Math.round(getValue() * 100) / 100 + '%', + footer: ({ column }) => { + const value = column.getAggregationValue() + return value === undefined + ? null + : `${Math.round(value * 100) / 100}%` + }, + }), + ]), + [], + ) + + const [data, setData] = useState(() => makeData(10_000)) + const refreshData = () => setData(() => makeData(10_000)) + const stressTest = () => setData(() => makeData(1_000_000)) + + const table = useAppTable( + { + columns, + data, + // initialState: { grouping: ['status'] }, // group by a column on first render + // atoms: { grouping: groupingAtom }, // preferred: own grouping state with an external atom + // state: { grouping }, // classic controlled state; pair with onGroupingChange + // onGroupingChange: setGrouping, + // enableGrouping: false, // disable grouping for every column; default true + // groupedColumnMode: 'remove', // remove grouped columns instead of moving them to the start; default 'reorder' + // manualGrouping: true, // pass rows that are already grouped and aggregated, for example from a server + debugTable: true, + }, + (state) => state, // default selector + ) + +
+
+ + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + return ( + + {row.getAllCells().map((cell) => { + return ( + + ) + })} + + ) + })} + + + {table.getFooterGroups().map((footerGroup) => ( + + {footerGroup.headers.map((header) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( +
+ {header.column.getCanGroup() ? ( + // If the header can be grouped, let's add a toggle + + ) : null}{' '} + +
+ )} +
+ {cell.getIsGrouped() ? ( + // If it's a grouped cell, add an expander and row count + <> + + + ) : cell.getIsAggregated() ? ( + // If the cell is aggregated, use the Aggregated + // renderer for cell + + ) : cell.getIsPlaceholder() ? null : ( // For cells with repeated values, render null + // Otherwise, just render the regular cell + + )} +
+ {header.isPlaceholder ? null : ( + + )} +
+
+
+ + + + + +
Page
+ + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + { + const page = (e.target as HTMLInputElement).value + ? Number((e.target as HTMLInputElement).value) - 1 + : 0 + table.setPageIndex(page) + }} + className="page-size-input" + /> + + +
+
{String(table.getRowModel().rows.length.toLocaleString())} Rows
+
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/grouped-aggregation/src/makeData.ts b/examples/octane/grouped-aggregation/src/makeData.ts new file mode 100644 index 0000000000..95302cdf16 --- /dev/null +++ b/examples/octane/grouped-aggregation/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/grouped-aggregation/src/vite-env.d.ts b/examples/octane/grouped-aggregation/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/grouped-aggregation/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/grouped-aggregation/tests/e2e/smoke.spec.ts b/examples/octane/grouped-aggregation/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..868e818817 --- /dev/null +++ b/examples/octane/grouped-aggregation/tests/e2e/smoke.spec.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' +import path from 'node:path' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return { errors, server } +} + +function getTable(page: Page) { + return page + .locator('table:visible, .divTable:visible') + .filter({ has: page.locator('thead th, .thead .th') }) + .filter({ has: page.locator('tbody tr, .tbody .tr') }) + .first() +} + +function getHeaderCells(table: Locator) { + return table.locator('thead th, .thead .th') +} + +function getBodyRows(table: Locator) { + return table.locator('tbody tr, .tbody .tr') +} + +async function getFirstBodyRowData(table: Locator) { + const row = getBodyRows(table).first() + const firstInput = row + .locator('input:not([type="checkbox"]):not([type="radio"])') + .first() + + if ((await firstInput.count()) > 0 && (await firstInput.isVisible())) { + return firstInput.inputValue() + } + + const text = await row.textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const { errors, server } = await openExample(page) + + try { + const table = getTable(page) + const bodyRows = getBodyRows(table) + + await expect(table).toBeVisible() + await expect(getHeaderCells(table).first()).toBeVisible() + await expect(bodyRows.first()).toBeVisible() + + const regenerateButton = page + .getByRole('button', { name: /^Regenerate Data$/i }) + .first() + + if ((await regenerateButton.count()) > 0) { + await expect(regenerateButton).toBeVisible() + + const firstRowBefore = await getFirstBodyRowData(table) + + await regenerateButton.click() + + await expect + .poll(() => getFirstBodyRowData(table)) + .not.toBe(firstRowBefore) + await expect(bodyRows.first()).toBeVisible() + } + + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/grouped-aggregation/tsconfig.json b/examples/octane/grouped-aggregation/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/grouped-aggregation/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/grouped-aggregation/vite.config.ts b/examples/octane/grouped-aggregation/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/grouped-aggregation/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/grouping/index.html b/examples/octane/grouping/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/grouping/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/grouping/package.json b/examples/octane/grouping/package.json new file mode 100644 index 0000000000..a91f3bd106 --- /dev/null +++ b/examples/octane/grouping/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-grouping", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/grouping/src/index.css b/examples/octane/grouping/src/index.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/octane/grouping/src/index.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/grouping/src/main.tsrx b/examples/octane/grouping/src/main.tsrx new file mode 100644 index 0000000000..35203520da --- /dev/null +++ b/examples/octane/grouping/src/main.tsrx @@ -0,0 +1,260 @@ +import { useMemo, useState } from 'octane' +import { createRoot } from 'octane' +import './index.css' +import { + columnFilteringFeature, + columnGroupingFeature, + createExpandedRowModel, + createFilteredRowModel, + createGroupedRowModel, + createPaginatedRowModel, + createSortedRowModel, + createTableHook, + filterFn_inNumberRange, + filterFn_includesString, + rowExpandingFeature, + rowPaginationFeature, + rowSortingFeature, + sortFn_alphanumeric, + sortFn_text, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Person } from './makeData' + +// this example happens to use the createTableHook pattern, but it is not required +const { useAppTable, createAppColumnHelper } = createTableHook({ + features: { + columnFilteringFeature, + columnGroupingFeature, + rowExpandingFeature, + rowPaginationFeature, + rowSortingFeature, + expandedRowModel: createExpandedRowModel(), + filteredRowModel: createFilteredRowModel(), + groupedRowModel: createGroupedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortedRowModel: createSortedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, + sortFns: { + alphanumeric: sortFn_alphanumeric, + text: sortFn_text, + }, + }, +}) + +const columnHelper = createAppColumnHelper() + +function App() @{ + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('firstName', { + header: 'First Name', + cell: (info) => info.getValue(), + /** + * override the value used for row grouping + * (otherwise, defaults to the value derived from accessorKey / accessorFn) + */ + getGroupingValue: (row) => `${row.firstName} ${row.lastName}`, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + header: () => Last Name, + cell: (info) => info.getValue(), + }), + columnHelper.accessor('age', { + header: () => 'Age', + }), + columnHelper.accessor('visits', { + header: () => Visits, + }), + columnHelper.accessor('status', { + header: 'Status', + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + cell: ({ getValue }) => + Math.round(getValue() * 100) / 100 + '%', + }), + ]), + [], + ) + + const [data, setData] = useState(() => makeData(10_000)) + const refreshData = () => setData(() => makeData(10_000)) + const stressTest = () => setData(() => makeData(1_000_000)) + + const table = useAppTable( + { + columns, + data, + // initialState: { grouping: ['status'] }, // group by a column on first render + // atoms: { grouping: groupingAtom }, // preferred: own grouping state with an external atom + // state: { grouping }, // classic controlled state; pair with onGroupingChange + // onGroupingChange: setGrouping, + // enableGrouping: false, // disable grouping for every column; default true + // groupedColumnMode: 'remove', // remove grouped columns instead of moving them to the start; default 'reorder' + // manualGrouping: true, // pass rows that are already grouped, for example from a server + debugTable: true, + }, + (state) => state, // default selector + ) + +
+
+ + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + return ( + + {row.getAllCells().map((cell) => { + return ( + + ) + })} + + ) + })} + +
+ {header.isPlaceholder ? null : ( +
+ {header.column.getCanGroup() ? ( + // If the header can be grouped, let's add a toggle + + ) : null}{' '} + +
+ )} +
+ {cell.getIsGrouped() ? ( + // If it's a grouped cell, add an expander and row count + <> + + + ) : cell.getIsPlaceholder() ? null : row.getIsGrouped() ? null : ( // For cells with repeated values, render null + // Otherwise, just render the regular cell + + )} +
+
+
+ + + + + +
Page
+ + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + { + const page = (e.target as HTMLInputElement).value + ? Number((e.target as HTMLInputElement).value) - 1 + : 0 + table.setPageIndex(page) + }} + className="page-size-input" + /> + + +
+
{String(table.getRowModel().rows.length.toLocaleString())} Rows
+
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/grouping/src/makeData.ts b/examples/octane/grouping/src/makeData.ts new file mode 100644 index 0000000000..95302cdf16 --- /dev/null +++ b/examples/octane/grouping/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/grouping/src/vite-env.d.ts b/examples/octane/grouping/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/grouping/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/grouping/tests/e2e/smoke.spec.ts b/examples/octane/grouping/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..10483bb8c5 --- /dev/null +++ b/examples/octane/grouping/tests/e2e/smoke.spec.ts @@ -0,0 +1,209 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +const TOTAL_ROWS = 10_000 +const STATUS_COUNT = 3 + +let server: Awaited> | undefined + +test.beforeAll(async () => { + // One server for the whole file. A cold Vite start costs more than a test. + test.setTimeout(180_000) + server = await startExampleServer(exampleDir) +}) + +test.afterAll(async () => { + await server?.close() +}) + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + if (!server) throw new Error('Example server failed to start') + + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return errors +} + +/** Each header carries a toggle: `👊` when ungrouped, `🛑(index)` when grouped. */ +function groupToggle(page: Page, header: string) { + return page + .locator('thead th') + .filter({ hasText: header }) + .getByRole('button') +} + +function bodyRows(page: Page) { + return page.locator('tbody tr') +} + +/** Row data is random faker output, so `table.state` is the stable oracle. */ +async function readState(page: Page) { + const text = await page.getByTestId('table-state').textContent() + + return JSON.parse(text ?? '{}') as { + grouping?: Array + expanded?: Record | true + } +} + +async function expectGrouping(page: Page, expected: Array) { + await expect + .poll(async () => (await readState(page)).grouping ?? []) + .toEqual(expected) +} + +/** The `(1,234)` leaf count rendered inside each group row's expander. */ +async function readGroupCounts(page: Page) { + const texts = await bodyRows(page).locator('td button').allTextContents() + + return texts.map((text) => { + const match = text.match(/\(([\d,]+)\)/) + return match ? Number((match[1] ?? '0').replace(/,/g, '')) : 0 + }) +} + +async function getFirstBodyRowText(page: Page) { + const text = await bodyRows(page).first().textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const errors = await openExample(page) + const table = page.locator('table').first() + + await expect(table).toBeVisible() + await expect(table.locator('thead th')).toHaveCount(6) + await expect(bodyRows(page).first()).toBeVisible() + await expectGrouping(page, []) + // Every header offers a grouping toggle, all of them ungrouped. + await expect(page.locator('thead th button')).toHaveCount(6) + await expect(groupToggle(page, 'Status')).toHaveText('👊') + + expect(errors).toEqual([]) +}) + +test('regenerates table data', async ({ page }) => { + const errors = await openExample(page) + const regenerateButton = page.getByRole('button', { + name: /^Regenerate Data$/i, + }) + + await expect(bodyRows(page).first()).toBeVisible() + + const firstRowBefore = await getFirstBodyRowText(page) + + await regenerateButton.click() + + await expect.poll(() => getFirstBodyRowText(page)).not.toBe(firstRowBefore) + + expect(errors).toEqual([]) +}) + +test('groups rows by a column', async ({ page }) => { + const errors = await openExample(page) + + await groupToggle(page, 'Status').click() + + await expectGrouping(page, ['status']) + // The toggle now reports this column's position in the grouping list. + await expect(groupToggle(page, 'Status')).toHaveText('🛑(0)') + + // Three statuses collapse ten thousand rows into three group rows. + await expect(bodyRows(page)).toHaveCount(STATUS_COUNT) + await expect(bodyRows(page).first()).toContainText('👉') + await expect(bodyRows(page).first()).toContainText(/\([\d,]+\)/) + + expect(errors).toEqual([]) +}) + +test('group leaf counts add up to every row', async ({ page }) => { + const errors = await openExample(page) + + await groupToggle(page, 'Status').click() + await expectGrouping(page, ['status']) + await expect(bodyRows(page)).toHaveCount(STATUS_COUNT) + + // Grouping partitions the rows, so the parts must make up the whole. This + // holds no matter what the random data happens to be. + const counts = await readGroupCounts(page) + expect(counts).toHaveLength(STATUS_COUNT) + expect(counts.reduce((total, count) => total + count, 0)).toBe(TOTAL_ROWS) + + expect(errors).toEqual([]) +}) + +test('expands a group to reveal its leaf rows', async ({ page }) => { + const errors = await openExample(page) + + await groupToggle(page, 'Status').click() + await expect(bodyRows(page)).toHaveCount(STATUS_COUNT) + + await bodyRows(page).first().locator('td button').first().click() + + await expect(bodyRows(page).first()).toContainText('👇') + // Leaf rows now sit under the expanded group, so the page fills up. + await expect.poll(() => bodyRows(page).count()).toBeGreaterThan(STATUS_COUNT) + + await bodyRows(page).first().locator('td button').first().click() + + await expect(bodyRows(page).first()).toContainText('👉') + await expect(bodyRows(page)).toHaveCount(STATUS_COUNT) + + expect(errors).toEqual([]) +}) + +test('nests a second grouping level', async ({ page }) => { + const errors = await openExample(page) + + await groupToggle(page, 'Status').click() + await expectGrouping(page, ['status']) + + await groupToggle(page, 'Age').click() + + await expectGrouping(page, ['status', 'age']) + await expect(groupToggle(page, 'Status')).toHaveText('🛑(0)') + await expect(groupToggle(page, 'Age')).toHaveText('🛑(1)') + // The outer grouping still decides the top level row count. + await expect(bodyRows(page)).toHaveCount(STATUS_COUNT) + + expect(errors).toEqual([]) +}) + +test('ungroups a column', async ({ page }) => { + const errors = await openExample(page) + + await groupToggle(page, 'Status').click() + await expectGrouping(page, ['status']) + await expect(bodyRows(page)).toHaveCount(STATUS_COUNT) + + await groupToggle(page, 'Status').click() + + await expectGrouping(page, []) + await expect(groupToggle(page, 'Status')).toHaveText('👊') + // Back to flat rows, so the page fills up again. + await expect(bodyRows(page)).toHaveCount(10) + + expect(errors).toEqual([]) +}) diff --git a/examples/octane/grouping/tsconfig.json b/examples/octane/grouping/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/grouping/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/grouping/vite.config.ts b/examples/octane/grouping/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/grouping/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/header-groups/index.html b/examples/octane/header-groups/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/header-groups/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/header-groups/package.json b/examples/octane/header-groups/package.json new file mode 100644 index 0000000000..1584a18494 --- /dev/null +++ b/examples/octane/header-groups/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-header-groups", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/header-groups/src/index.css b/examples/octane/header-groups/src/index.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/octane/header-groups/src/index.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/header-groups/src/main.tsrx b/examples/octane/header-groups/src/main.tsrx new file mode 100644 index 0000000000..af5f90dd87 --- /dev/null +++ b/examples/octane/header-groups/src/main.tsrx @@ -0,0 +1,129 @@ +import { useState } from 'octane' +import { createRoot } from 'octane' +import './index.css' +import { + createColumnHelper, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Person } from './makeData' + +const features = tableFeatures({}) + +const columnHelper = createColumnHelper() + +// use new columnHelper.columns method to create columns with the same TValue generic so TypeScript doesn't complain when passing columns to useTable +const columns = columnHelper.columns([ + columnHelper.group({ + id: 'hello', + header: () => Hello, + columns: columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + footer: (props) => props.column.id, + }), + ]), + }), + columnHelper.group({ + header: 'Info', + footer: (props) => props.column.id, + columns: columnHelper.columns([ + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + }), + columnHelper.group({ + header: 'More Info', + columns: columnHelper.columns([ + columnHelper.accessor('visits', { + header: () => Visits, + footer: (props) => props.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + }), + ]), + }), + ]), + }), +]) + +function App() @{ + const [data, setData] = useState(() => makeData(20)) + const refreshData = () => setData(makeData(20)) + const stressTest = () => setData(makeData(1_000)) + + const table = useTable( + { + debugTable: true, + features, + columns, + data, + }, + (state) => state, // default selector + ) + +
+
+ + +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getAllCells().map((cell) => ( + + ))} + + ))} + + + {table.getFooterGroups().map((footerGroup) => ( + + {footerGroup.headers.map((header) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( + + )} +
+ +
+ {header.isPlaceholder ? null : ( + + )} +
+
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/header-groups/src/makeData.ts b/examples/octane/header-groups/src/makeData.ts new file mode 100644 index 0000000000..c0f87b8b19 --- /dev/null +++ b/examples/octane/header-groups/src/makeData.ts @@ -0,0 +1,46 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + status: string + progress: number +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/header-groups/src/vite-env.d.ts b/examples/octane/header-groups/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/header-groups/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/header-groups/tests/e2e/smoke.spec.ts b/examples/octane/header-groups/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..c6219276a9 --- /dev/null +++ b/examples/octane/header-groups/tests/e2e/smoke.spec.ts @@ -0,0 +1,169 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +const LEAF_HEADERS = [ + 'firstName', + 'Last Name', + 'Age', + 'Visits', + 'Status', + 'Profile Progress', +] + +const LEAF_FOOTERS = [ + 'firstName', + 'lastName', + 'age', + 'visits', + 'status', + 'progress', +] + +let server: Awaited> | undefined + +test.beforeAll(async () => { + // One server for the whole file. A cold Vite start costs more than a test. + test.setTimeout(180_000) + server = await startExampleServer(exampleDir) +}) + +test.afterAll(async () => { + await server?.close() +}) + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + if (!server) throw new Error('Example server failed to start') + + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return errors +} + +function headerRow(page: Page, index: number) { + return page.locator('thead tr').nth(index).locator('th') +} + +function footerRow(page: Page, index: number) { + return page.locator('tfoot tr').nth(index).locator('th') +} + +async function readColSpans(page: Page, selector: string) { + return page + .locator(selector) + .evaluateAll((cells) => + cells.map((cell) => Number(cell.getAttribute('colspan') ?? '1')), + ) +} + +async function getFirstBodyRowText(page: Page) { + const text = await page.locator('tbody tr').first().textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const errors = await openExample(page) + const table = page.locator('table').first() + + await expect(table).toBeVisible() + await expect(page.locator('tbody tr')).toHaveCount(20) + await expect(page.locator('tbody tr').first().locator('td')).toHaveCount( + LEAF_HEADERS.length, + ) + + expect(errors).toEqual([]) +}) + +test('regenerates table data', async ({ page }) => { + const errors = await openExample(page) + const regenerateButton = page.getByRole('button', { + name: /^Regenerate Data$/i, + }) + + await expect(page.locator('tbody tr').first()).toBeVisible() + + const firstRowBefore = await getFirstBodyRowText(page) + + await regenerateButton.click() + + await expect.poll(() => getFirstBodyRowText(page)).not.toBe(firstRowBefore) + await expect(page.locator('tbody tr')).toHaveCount(20) + + expect(errors).toEqual([]) +}) + +test('nests the header groups three rows deep', async ({ page }) => { + const errors = await openExample(page) + + // Two top level groups, one of which nests a further group, so the deepest + // leaf needs three header rows. + await expect(page.locator('thead tr')).toHaveCount(3) + + await expect(headerRow(page, 0)).toHaveText(['Hello', 'Info']) + // Row two is mostly placeholders for the columns that have no middle group. + await expect(headerRow(page, 1)).toHaveText(['', '', '', 'More Info']) + await expect(headerRow(page, 2)).toHaveText(LEAF_HEADERS) + + expect(errors).toEqual([]) +}) + +test('spans each group across the columns beneath it', async ({ page }) => { + const errors = await openExample(page) + + // `Hello` covers two leaves, `Info` covers the other four. + expect(await readColSpans(page, 'thead tr:nth-child(1) th')).toEqual([2, 4]) + // `More Info` covers the last three of `Info`'s four leaves. + expect(await readColSpans(page, 'thead tr:nth-child(2) th')).toEqual([ + 1, 1, 1, 3, + ]) + expect(await readColSpans(page, 'thead tr:nth-child(3) th')).toEqual([ + 1, 1, 1, 1, 1, 1, + ]) + + // Every header row must cover the same total width as the body rows. + for (const row of [1, 2, 3]) { + const spans = await readColSpans(page, `thead tr:nth-child(${row}) th`) + expect(spans.reduce((total, span) => total + span, 0)).toBe( + LEAF_HEADERS.length, + ) + } + + expect(errors).toEqual([]) +}) + +test('mirrors the header groups in the footer', async ({ page }) => { + const errors = await openExample(page) + + // Footer groups are the header groups reversed, so leaves come first. + await expect(page.locator('tfoot tr')).toHaveCount(3) + await expect(footerRow(page, 0)).toHaveText(LEAF_FOOTERS) + + // The `hello` group declares a header but no footer, so its footer cell is + // empty, while `Info` declares both. The cell is still rendered and still + // spans its columns, which is what keeps the footer aligned with the body. + await expect(footerRow(page, 2)).toHaveText(['', 'Info']) + expect(await readColSpans(page, 'tfoot tr:nth-child(3) th')).toEqual([2, 4]) + + expect(errors).toEqual([]) +}) diff --git a/examples/octane/header-groups/tsconfig.json b/examples/octane/header-groups/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/header-groups/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/header-groups/vite.config.ts b/examples/octane/header-groups/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/header-groups/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/kitchen-sink/index.html b/examples/octane/kitchen-sink/index.html new file mode 100644 index 0000000000..695c53b2cc --- /dev/null +++ b/examples/octane/kitchen-sink/index.html @@ -0,0 +1,13 @@ + + + + + + + TanStack Table - Kitchen Sink + + +
+ + + diff --git a/examples/octane/kitchen-sink/package.json b/examples/octane/kitchen-sink/package.json new file mode 100644 index 0000000000..39e2939644 --- /dev/null +++ b/examples/octane/kitchen-sink/package.json @@ -0,0 +1,24 @@ +{ + "name": "@tanstack/octane-table-example-kitchen-sink", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/match-sorter-utils": "^9.0.0-beta.53", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/kitchen-sink/src/index.css b/examples/octane/kitchen-sink/src/index.css new file mode 100644 index 0000000000..0209b069a8 --- /dev/null +++ b/examples/octane/kitchen-sink/src/index.css @@ -0,0 +1,317 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +.table-container { + overflow-x: auto; + width: 100%; + max-width: 1400px; + border: 1px solid lightgray; +} + +table { + /* box-shadow / borders + position: sticky don't play well otherwise */ + border-collapse: collapse; + border-spacing: 0; + table-layout: fixed; +} + +thead { + position: relative; +} + +th { + background-color: #f3f4f6; + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + font-weight: bold; + padding: 2px 4px; + position: relative; + text-align: left; + vertical-align: top; +} + +td { + background-color: white; + border-bottom: 1px solid #eee; + padding: 2px 4px; + vertical-align: middle; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Resizer (column resizing handle) */ +.resizer { + position: absolute; + top: 0; + right: 0; + height: 100%; + width: 5px; + background: rgba(0, 0, 0, 0.5); + cursor: col-resize; + user-select: none; + touch-action: none; + z-index: 2; +} + +.resizer.isResizing { + background: blue; + opacity: 1; +} + +@media (hover: hover) { + .resizer { + opacity: 0; + } + + *:hover > .resizer { + opacity: 1; + } +} + +/* Demo layout utilities. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions, +.header-row, +.header-controls { + display: flex; + align-items: center; +} + +.header-row { + gap: 0.25rem; + flex-wrap: nowrap; +} + +.header-controls { + gap: 0.125rem; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.toolbar { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; + border: 1px solid lightgray; + margin-bottom: 0.5rem; +} + +.toolbar-row { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.25rem; +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; + margin-bottom: 0.25rem; +} + +.column-toggle-row { + padding: 0 0.25rem; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.icon-button { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button, +.icon-button { + padding: 0 0.25rem; + cursor: pointer; + background: transparent; + font-size: 0.875rem; +} + +.icon-button:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.nowrap { + white-space: nowrap; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.global-filter-input { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; + width: 100%; + max-width: 24rem; +} + +/* Sortable / draggable header affordances */ +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.drag-handle { + cursor: grab; + padding: 0 0.25rem; +} + +.drag-handle:active { + cursor: grabbing; +} + +/* Grouped / aggregated / placeholder cell backgrounds (from grouping example) */ +.cell-grouped { + background: #0aff0082; +} + +.cell-aggregated { + background: #ffa50078; +} + +.cell-placeholder { + background: #ff000042; +} + +/* Row pinning — light blue tint + outline so pinned rows are obvious + * against sticky positioning. Outline applied via inset box-shadow on each + * cell (outline on doesn't render well with border-collapse: collapse). */ +.pinned-row td { + background-color: rgba(135, 206, 235, 0.18); + box-shadow: + inset 0 2px 0 #4aa3df, + inset 0 -2px 0 #4aa3df; +} +.pinned-row td:first-child { + box-shadow: + inset 2px 0 0 #4aa3df, + inset 0 2px 0 #4aa3df, + inset 0 -2px 0 #4aa3df; +} +.pinned-row td:last-child { + box-shadow: + inset -2px 0 0 #4aa3df, + inset 0 2px 0 #4aa3df, + inset 0 -2px 0 #4aa3df; +} + +.label-offset { + margin-left: 0.5rem; +} + +.state-dump { + background: #f3f4f6; + border: 1px solid lightgray; + border-radius: 0.25rem; + padding: 0.5rem; + font-size: 0.75rem; + max-height: 24rem; + overflow: auto; +} diff --git a/examples/octane/kitchen-sink/src/main.tsrx b/examples/octane/kitchen-sink/src/main.tsrx new file mode 100644 index 0000000000..d9ce4f4ce2 --- /dev/null +++ b/examples/octane/kitchen-sink/src/main.tsrx @@ -0,0 +1,779 @@ +import { createRoot } from 'octane' +import { useEffect, useMemo, useRef, useState } from 'octane' +import { faker } from '@faker-js/faker' +import { + aggregationFn_mean, + aggregationFn_median, + aggregationFn_sum, + createColumnHelper, + createExpandedRowModel, + createFacetedMinMaxValues, + createFacetedRowModel, + createFacetedUniqueValues, + createFilteredRowModel, + createGroupedRowModel, + createPaginatedRowModel, + createSortedRowModel, + filterFn_inNumberRange, + filterFn_includesString, + metaHelper, + sortFn_alphanumeric, + sortFn_text, + stockFeatures, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { compareItems, rankItem } from '@tanstack/match-sorter-utils' +import { makeData } from './makeData' +import type { JSX, Octane } from 'octane/jsx-runtime' +import type { RankingInfo } from '@tanstack/match-sorter-utils' +import type { Person } from './makeData' +import type { + Cell, + Column, + FilterFn, + Header, + OctaneTable, + Row, + SortFn, + TableFeatures, +} from '@tanstack/octane-table' +import './index.css' + +type Style = Extract, object> + +interface MyColumnMeta { + filterVariant?: 'text' | 'range' | 'select' +} + +// The filter meta that the fuzzy filter attaches to rows, declared per-table +// via the `filterMeta` slot below. No declaration merging needed! +interface FuzzyFilterMeta { + itemRank?: RankingInfo +} + +// Broad features type for writing the custom fns below before the `features` +// object exists, with the filter meta type plugged in +type FuzzyFeatures = TableFeatures & { filterMeta: FuzzyFilterMeta } + +const fuzzyFilter: FilterFn = ( + row, + columnId, + value, + addMeta, +) => { + const itemRank = rankItem(row.getValue(columnId), value) + addMeta?.({ itemRank }) + return itemRank.passed +} + +const fuzzySort: SortFn = (rowA, rowB, columnId) => { + let dir = 0 + if (rowA.columnFiltersMeta[columnId]) { + dir = compareItems( + rowA.columnFiltersMeta[columnId].itemRank!, + rowB.columnFiltersMeta[columnId].itemRank!, + ) + } + return dir === 0 ? sortFn_alphanumeric(rowA, rowB, columnId) : dir +} + +const features = tableFeatures({ + ...stockFeatures, + expandedRowModel: createExpandedRowModel(), + filteredRowModel: createFilteredRowModel(), + facetedRowModel: createFacetedRowModel(), + facetedMinMaxValues: createFacetedMinMaxValues(), + facetedUniqueValues: createFacetedUniqueValues(), + groupedRowModel: createGroupedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + sortedRowModel: createSortedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + fuzzy: fuzzyFilter, + }, + sortFns: { + alphanumeric: sortFn_alphanumeric, + text: sortFn_text, + fuzzy: fuzzySort, + sortStatus: (rowA: any, rowB: any) => { + const statusOrder = ['single', 'complicated', 'relationship'] + return ( + statusOrder.indexOf(rowA.original.status) - + statusOrder.indexOf(rowB.original.status) + ) + }, + }, + aggregationFns: { + mean: aggregationFn_mean, + median: aggregationFn_median, + sum: aggregationFn_sum, + }, + filterMeta: metaHelper(), + columnMeta: metaHelper(), +}) + +const getCommonPinningStyles = ( + column: Column, +): Style => { + const isPinned = column.getIsPinned() + const isLastLeftPinnedColumn = + isPinned === 'start' && column.getIsLastColumn('start') + const isFirstRightPinnedColumn = + isPinned === 'end' && column.getIsFirstColumn('end') + + return { + boxShadow: isLastLeftPinnedColumn + ? '-4px 0 4px -4px gray inset' + : isFirstRightPinnedColumn + ? '4px 0 4px -4px gray inset' + : undefined, + insetInlineStart: + isPinned === 'start' ? `${column.getStart('start')}px` : undefined, + insetInlineEnd: + isPinned === 'end' ? `${column.getAfter('end')}px` : undefined, + opacity: isPinned ? 0.97 : 1, + position: isPinned ? 'sticky' : 'relative', + zIndex: isPinned ? 1 : 0, + } +} + +function IndeterminateCheckbox({ + indeterminate, + className = '', + ...rest +}: { indeterminate?: boolean } & Octane.InputHTMLAttributes) @{ + const ref = useRef(null) + + useEffect(() => { + if (ref.current && typeof indeterminate === 'boolean') { + ref.current.indeterminate = !rest.checked && indeterminate + } + }, [indeterminate, rest.checked]) + + +} + +function DebouncedInput({ + value: initialValue, + onChange, + debounce = 300, + ...props +}: { + value: string | number + onChange: (value: string | number) => void + debounce?: number +} & Omit, 'onChange' | 'value'>) @{ + const [value, setValue] = useState(initialValue) + const timeoutRef = useRef | undefined>( + undefined, + ) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + return () => clearTimeout(timeoutRef.current) + }, []) + + { + const nextValue = e.currentTarget.value + setValue(nextValue) + clearTimeout(timeoutRef.current) + timeoutRef.current = setTimeout(() => { + onChange(nextValue) + }, debounce) + }} + /> +} + +function Filter({ column }: { column: Column }) { + const { filterVariant } = column.columnDef.meta ?? {} + const columnFilterValue = column.getFilterValue() + const minMaxValues = + filterVariant === 'range' ? column.getFacetedMinMaxValues() : undefined + const sortedUniqueValues = useMemo( + () => + filterVariant === 'range' + ? [] + : Array.from(column.getFacetedUniqueValues().keys()) + .sort() + .slice(0, 5000), + [column.getFacetedUniqueValues(), filterVariant], + ) + + return filterVariant === 'range' ? ( +
+ + column.setFilterValue((old: [number, number] | undefined) => [ + value, + old?.[1], + ]) + } + placeholder={`Min${minMaxValues?.[0] !== undefined ? ` (${minMaxValues[0]})` : ''}`} + className="filter-input" + /> + + column.setFilterValue((old: [number, number] | undefined) => [ + old?.[0], + value, + ]) + } + placeholder={`Max${minMaxValues?.[1] !== undefined ? ` (${minMaxValues[1]})` : ''}`} + className="filter-input" + /> +
+ ) : filterVariant === 'select' ? ( + + ) : ( + <> + + {sortedUniqueValues.map((value) => ( + + column.setFilterValue(value)} + placeholder={`Search (${column.getFacetedUniqueValues().size})`} + className="filter-select" + list={column.id + 'list'} + /> + + ) +} + +function TableHeader({ + header, + table, +}: { + header: Header + table: OctaneTable +}) @{ + const column = header.column + const style: Style = { + ...getCommonPinningStyles(column), + whiteSpace: 'nowrap', + width: `calc(var(--header-${header.id}-size) * 1px)`, + } + + if (header.isPlaceholder) { + return + } + + +
+
+
+ {column.getCanPin() ? ( + + {column.getIsPinned() !== 'start' ? ( + + ) : null} + {column.getIsPinned() ? ( + + ) : null} + {column.getIsPinned() !== 'end' ? ( + + ) : null} + + ) : null} + {column.getCanGroup() ? ( + + ) : null} +
+ {column.getCanSort() ? ( + + + {{ + asc: ' ▲', + desc: ' ▼', + }[column.getIsSorted() as string] ?? null} + + ) : ( + + )} + {column.getCanFilter() ? ( +
+ +
+ ) : null} +
+
+ {column.getCanResize() ? ( +
column.resetSize()} + onMouseDown={header.getResizeHandler()} + onTouchStart={header.getResizeHandler()} + className={`resizer ${column.getIsResizing() ? 'isResizing' : ''}`} + /> + ) : null} + +} + +function TableCell({ + cell, + table, +}: { + cell: Cell + table: OctaneTable +}) @{ + const groupingActive = table.state.grouping.length > 0 + const hasAggregation = !!cell.column.columnDef.aggregationFn + const className = !groupingActive + ? undefined + : cell.getIsGrouped() + ? 'cell-grouped' + : hasAggregation && cell.getIsAggregated() + ? 'cell-aggregated' + : cell.getIsPlaceholder() + ? 'cell-placeholder' + : undefined + + + {cell.getIsGrouped() ? ( + + ) : ( + + )} + +} + +function PinnedRow({ + row, + table, +}: { + row: Row + table: OctaneTable +}) @{ + const bottomRows = table.getBottomRows() + + {row.getVisibleCells().map((cell) => ( + + ))} + +} + +function App() @{ + const columns = useMemo(() => { + const columnHelper = createColumnHelper() + return columnHelper.columns([ + columnHelper.display({ + id: 'select', + size: 80, + minSize: 80, + maxSize: 80, + enableSorting: false, + enableGrouping: false, + enableHiding: false, + enableResizing: false, + header: ({ table }) => ( + + ), + cell: ({ row }) => ( +
+ {' '} + +
+ ), + }), + columnHelper.accessor('firstName', { + id: 'firstName', + size: 200, + header: 'First Name', + filterFn: 'fuzzy', + sortFn: 'fuzzy', + meta: { filterVariant: 'text' }, + getGroupingValue: (row) => `${row.firstName} ${row.lastName}`, + cell: ({ row, getValue }) => ( +
+ {/* On a grouped row the grouped-cell wrapper already renders the + expander, so don't render a second one here. This expander is + only for expanding nested sub-rows. */} + {row.getIsGrouped() ? null : row.getCanExpand() ? ( + + ) : ( + - + )} + {String(getValue())} +
+ ), + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + size: 180, + header: 'Last Name', + meta: { filterVariant: 'text' }, + }), + columnHelper.accessor('age', { + id: 'age', + size: 200, + header: 'Age', + meta: { filterVariant: 'range' }, + aggregationFn: 'median', + aggregatedCell: ({ getValue }) => + Math.round(getValue() * 100) / 100, + }), + columnHelper.accessor('visits', { + id: 'visits', + size: 200, + header: 'Visits', + meta: { filterVariant: 'range' }, + aggregationFn: 'sum', + aggregatedCell: ({ getValue }) => getValue().toLocaleString(), + }), + columnHelper.accessor('status', { + id: 'status', + size: 200, + header: 'Status', + sortFn: 'sortStatus', + meta: { filterVariant: 'select' }, + }), + columnHelper.accessor('progress', { + id: 'progress', + size: 200, + header: 'Profile Progress', + meta: { filterVariant: 'range' }, + aggregationFn: 'mean', + cell: ({ getValue }) => + `${Math.round(getValue() * 100) / 100}%`, + aggregatedCell: ({ getValue }) => + `${Math.round(getValue() * 100) / 100}%`, + }), + ]) + }, []) + + const [data, setData] = useState(() => makeData(1_000)) + const refreshData = () => setData(makeData(1_000)) + const nestedData = () => setData(makeData(100, 5, 3)) + const stress10k = () => setData(makeData(10_000)) + const stress100k = () => setData(makeData(100_000)) + + const table = useTable( + { + features, + columns, + data, + getSubRows: (row) => row.subRows, + globalFilterFn: 'fuzzy', + columnResizeMode: 'onChange', + defaultColumn: { minSize: 200, maxSize: 800 }, + initialState: { + columnOrder: columns.map((c) => c.id!), + columnPinning: { start: ['select'], end: [] }, + pagination: { pageIndex: 0, pageSize: 20 }, + }, + keepPinnedRows: true, + debugTable: true, + }, + (state) => state, // default selector + ) + + const columnSizeVars = useMemo(() => { + const headers = table.getFlatHeaders() + const colSizes: Record = {} + for (const header of headers) { + colSizes[`--header-${header.id}-size`] = header.getSize() + colSizes[`--col-${header.column.id}-size`] = header.column.getSize() + } + return colSizes + }, [table.state.columnResizing, table.state.columnSizing]) + + const shuffleColumns = () => { + table.setColumnOrder( + faker.helpers.shuffle(table.getAllLeafColumns().map((d) => d.id)), + ) + } + + const selectedCount = table.getSelectedRowModel().flatRows.length + +
+

Kitchen Sink - All Features

+
+
+ table.setGlobalFilter(String(value))} + className="global-filter-input" + placeholder="Fuzzy search all columns..." + /> +
+
+ + + + + + + + {selectedCount.toLocaleString()} of{' '} + {table.getCoreRowModel().flatRows.length.toLocaleString()} selected + +
+
+ + Column visibility + +
+ +
+ {table.getAllLeafColumns().map((column) => ( +
+ +
+ ))} +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getTopRows().map((row) => ( + + ))} + {table.getCenterRows().map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + {table.getBottomRows().map((row) => ( + + ))} + +
+
+
+
+ + + + + +
Page
+ + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + { + const page = e.currentTarget.value + ? Number(e.currentTarget.value) - 1 + : 0 + table.setPageIndex(page) + }} + className="page-size-input" + /> + + +
+
+
+ {table.getRowModel().rows.length.toLocaleString()} rows on this page ( + {table.getFilteredRowModel().rows.length.toLocaleString()} filtered of{' '} + {table.getCoreRowModel().rows.length.toLocaleString()} total) +
+
+
+ Table state (live) +
{JSON.stringify(table.state, null, 2)}
+
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/kitchen-sink/src/makeData.ts b/examples/octane/kitchen-sink/src/makeData.ts new file mode 100644 index 0000000000..95302cdf16 --- /dev/null +++ b/examples/octane/kitchen-sink/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/kitchen-sink/src/vite-env.d.ts b/examples/octane/kitchen-sink/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/kitchen-sink/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/kitchen-sink/tests/e2e/smoke.spec.ts b/examples/octane/kitchen-sink/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..868e818817 --- /dev/null +++ b/examples/octane/kitchen-sink/tests/e2e/smoke.spec.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' +import path from 'node:path' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' + +const exampleDir = path.resolve() + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + const server = await startExampleServer(exampleDir) + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return { errors, server } +} + +function getTable(page: Page) { + return page + .locator('table:visible, .divTable:visible') + .filter({ has: page.locator('thead th, .thead .th') }) + .filter({ has: page.locator('tbody tr, .tbody .tr') }) + .first() +} + +function getHeaderCells(table: Locator) { + return table.locator('thead th, .thead .th') +} + +function getBodyRows(table: Locator) { + return table.locator('tbody tr, .tbody .tr') +} + +async function getFirstBodyRowData(table: Locator) { + const row = getBodyRows(table).first() + const firstInput = row + .locator('input:not([type="checkbox"]):not([type="radio"])') + .first() + + if ((await firstInput.count()) > 0 && (await firstInput.isVisible())) { + return firstInput.inputValue() + } + + const text = await row.textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const { errors, server } = await openExample(page) + + try { + const table = getTable(page) + const bodyRows = getBodyRows(table) + + await expect(table).toBeVisible() + await expect(getHeaderCells(table).first()).toBeVisible() + await expect(bodyRows.first()).toBeVisible() + + const regenerateButton = page + .getByRole('button', { name: /^Regenerate Data$/i }) + .first() + + if ((await regenerateButton.count()) > 0) { + await expect(regenerateButton).toBeVisible() + + const firstRowBefore = await getFirstBodyRowData(table) + + await regenerateButton.click() + + await expect + .poll(() => getFirstBodyRowData(table)) + .not.toBe(firstRowBefore) + await expect(bodyRows.first()).toBeVisible() + } + + expect(errors).toEqual([]) + } finally { + await server.close() + } +}) diff --git a/examples/octane/kitchen-sink/tsconfig.json b/examples/octane/kitchen-sink/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/kitchen-sink/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/kitchen-sink/vite.config.ts b/examples/octane/kitchen-sink/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/kitchen-sink/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/pagination/index.html b/examples/octane/pagination/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/pagination/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/pagination/package.json b/examples/octane/pagination/package.json new file mode 100644 index 0000000000..4f49323010 --- /dev/null +++ b/examples/octane/pagination/package.json @@ -0,0 +1,24 @@ +{ + "name": "@tanstack/octane-table-example-pagination", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-store": "^0.12.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/pagination/src/index.css b/examples/octane/pagination/src/index.css new file mode 100644 index 0000000000..443b3983a6 --- /dev/null +++ b/examples/octane/pagination/src/index.css @@ -0,0 +1,369 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled { + opacity: 0.5; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/pagination/src/main.tsrx b/examples/octane/pagination/src/main.tsrx new file mode 100644 index 0000000000..9afd6fdfac --- /dev/null +++ b/examples/octane/pagination/src/main.tsrx @@ -0,0 +1,214 @@ +import { createRoot } from 'octane' +import { useMemo, useState } from 'octane' +import './index.css' +import { + createColumnHelper, + createPaginatedRowModel, + rowPaginationFeature, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Person } from './makeData' + +const features = tableFeatures({ + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), +}) + +const columnHelper = createColumnHelper() + +function App() @{ + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.display({ + id: 'rowNumber', + header: '#', + cell: ({ row }) => row.getDisplayIndex() + 1, + }), + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + footer: (props) => props.column.id, + }), + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + }), + columnHelper.accessor('visits', { + header: () => Visits, + footer: (props) => props.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + }), + ]), + [], + ) + + const [data, setData] = useState(() => makeData(1_000)) + const refreshData = () => setData(() => makeData(1_000)) + const stressTest = () => setData(() => makeData(1_000_000)) + + <> +
+ + +
+ +
+
+ +} + +function MyTable({ + data, + columns, +}: { + data: Array + columns: ReturnType +}) @{ + const table = useTable( + { + features, + columns, + data, + // initialState: { pagination: { pageIndex: 1, pageSize: 20 } }, // set the initial page once + // atoms: { pagination: paginationAtom }, // preferred: own pagination state with an external atom + // state: { pagination }, // classic controlled state; pair with onPaginationChange + // onPaginationChange: setPagination, + // autoResetPageIndex: false, // keep the current page after page-altering changes; default true + // autoResetAll: false, // turn off every feature's automatic reset, including page index + // manualPagination: true, // pass data that is already paginated, for example from a server + // pageCount: 10, // total pages for manual pagination; use -1 when unknown + // rowCount: 1_000, // total rows for manual pagination; pageCount is calculated from this and pageSize + debugTable: true, + }, + (state) => state, // default selector + ) + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + return ( + + {row.getAllCells().map((cell) => { + return ( + + ) + })} + + ) + })} + +
+
+ +
+
+ +
+
+
+ + + + + +
Page
+ + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + { + const page = (e.target as HTMLInputElement).value + ? Number((e.target as HTMLInputElement).value) - 1 + : 0 + table.setPageIndex(page) + }} + className="page-size-input" + /> + + +
+
+ Showing {table.getRowModel().rows.length.toLocaleString()} of{' '} + {String(table.getRowCount().toLocaleString())} Rows +
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/pagination/src/makeData.ts b/examples/octane/pagination/src/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/octane/pagination/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/pagination/src/vite-env.d.ts b/examples/octane/pagination/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/pagination/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/pagination/tests/e2e/smoke.spec.ts b/examples/octane/pagination/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..401d7920ec --- /dev/null +++ b/examples/octane/pagination/tests/e2e/smoke.spec.ts @@ -0,0 +1,278 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +/** `makeData(1_000)` at the default page size of 10 gives exactly 100 pages. */ +const TOTAL_ROWS = 1_000 +const PAGE_SIZES = [10, 20, 30, 40, 50] + +let server: Awaited> | undefined + +test.beforeAll(async () => { + // One server for the whole file. A cold Vite start costs more than a test. + test.setTimeout(180_000) + server = await startExampleServer(exampleDir) +}) + +test.afterAll(async () => { + await server?.close() +}) + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + if (!server) throw new Error('Example server failed to start') + + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return errors +} + +/** `<` is a prefix of `<<`, so every page button match has to be exact. */ +function pageButton(page: Page, label: '<<' | '<' | '>' | '>>') { + return page.getByRole('button', { name: label, exact: true }) +} + +/** The `{pageIndex + 1} of {pageCount}` readout. */ +function pageStatus(page: Page) { + return page.locator('.controls strong') +} + +function goToPageInput(page: Page) { + return page.getByRole('spinbutton') +} + +function pageSizeSelect(page: Page) { + return page.locator('.controls select') +} + +function rowCountLine(page: Page) { + return page.getByText(/Showing .* Rows/) +} + +function bodyRows(page: Page) { + return page.locator('tbody tr') +} + +/** Row data is random faker output, so `table.state` is the stable oracle. */ +async function readPagination(page: Page) { + const text = await page.getByTestId('table-state').textContent() + const state = JSON.parse(text ?? '{}') as { + pagination?: { pageIndex: number; pageSize: number } + } + + return state.pagination +} + +async function expectPagination( + page: Page, + expected: { pageIndex: number; pageSize: number }, +) { + await expect.poll(() => readPagination(page)).toEqual(expected) +} + +async function getFirstBodyRowText(page: Page) { + const text = await bodyRows(page).first().textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const errors = await openExample(page) + const table = page.locator('table').first() + + await expect(table).toBeVisible() + await expect(table.locator('thead th')).toHaveCount(7) + await expect(bodyRows(page)).toHaveCount(10) + await expect(rowCountLine(page)).toHaveText('Showing 10 of 1,000 Rows') + await expectPagination(page, { pageIndex: 0, pageSize: 10 }) + + expect(errors).toEqual([]) +}) + +test('regenerates table data', async ({ page }) => { + const errors = await openExample(page) + const regenerateButton = page.getByRole('button', { + name: /^Regenerate Data$/i, + }) + + await expect(bodyRows(page).first()).toBeVisible() + + const firstRowBefore = await getFirstBodyRowText(page) + + await regenerateButton.click() + + await expect.poll(() => getFirstBodyRowText(page)).not.toBe(firstRowBefore) + await expect(bodyRows(page)).toHaveCount(10) + + expect(errors).toEqual([]) +}) + +test('disables the first and previous buttons on the first page', async ({ + page, +}) => { + const errors = await openExample(page) + + await expect(pageStatus(page)).toHaveText('1 of 100') + await expect(pageButton(page, '<<')).toBeDisabled() + await expect(pageButton(page, '<')).toBeDisabled() + await expect(pageButton(page, '>')).toBeEnabled() + await expect(pageButton(page, '>>')).toBeEnabled() + + expect(errors).toEqual([]) +}) + +test('advances and rewinds one page at a time', async ({ page }) => { + const errors = await openExample(page) + + await expect(pageStatus(page)).toHaveText('1 of 100') + + await pageButton(page, '>').click() + await expect(pageStatus(page)).toHaveText('2 of 100') + await expectPagination(page, { pageIndex: 1, pageSize: 10 }) + // Reaching page two enables the backwards buttons. + await expect(pageButton(page, '<<')).toBeEnabled() + await expect(pageButton(page, '<')).toBeEnabled() + // The controlled input tracks the page rather than going stale. + await expect(goToPageInput(page)).toHaveValue('2') + + const secondPageFirstRow = await getFirstBodyRowText(page) + + await pageButton(page, '>').click() + await expect(pageStatus(page)).toHaveText('3 of 100') + + await pageButton(page, '<').click() + await expect(pageStatus(page)).toHaveText('2 of 100') + // Returning to a page shows the same rows, so paging does not reshuffle. + expect(await getFirstBodyRowText(page)).toBe(secondPageFirstRow) + + // The page window slides but its size and the total do not. + await expect(bodyRows(page)).toHaveCount(10) + await expect(rowCountLine(page)).toHaveText('Showing 10 of 1,000 Rows') + + expect(errors).toEqual([]) +}) + +test('jumps to the last and first pages', async ({ page }) => { + const errors = await openExample(page) + + await pageButton(page, '>>').click() + await expect(pageStatus(page)).toHaveText('100 of 100') + await expectPagination(page, { pageIndex: 99, pageSize: 10 }) + await expect(bodyRows(page)).toHaveCount(10) + await expect(pageButton(page, '>')).toBeDisabled() + await expect(pageButton(page, '>>')).toBeDisabled() + await expect(pageButton(page, '<<')).toBeEnabled() + await expect(pageButton(page, '<')).toBeEnabled() + + await pageButton(page, '<<').click() + await expect(pageStatus(page)).toHaveText('1 of 100') + await expectPagination(page, { pageIndex: 0, pageSize: 10 }) + await expect(pageButton(page, '<<')).toBeDisabled() + await expect(pageButton(page, '<')).toBeDisabled() + + expect(errors).toEqual([]) +}) + +test('goes to an arbitrary page via the Go to page input', async ({ page }) => { + const errors = await openExample(page) + const input = goToPageInput(page) + + await expect(input).toHaveAttribute('min', '1') + await expect(input).toHaveAttribute('max', '100') + await expect(input).toHaveValue('1') + + await input.fill('42') + await expect(pageStatus(page)).toHaveText('42 of 100') + await expectPagination(page, { pageIndex: 41, pageSize: 10 }) + + await input.fill('1') + await expect(pageStatus(page)).toHaveText('1 of 100') + await expect(pageButton(page, '<')).toBeDisabled() + + // Clearing the field falls back to the first page rather than NaN. + await input.fill('7') + await expect(pageStatus(page)).toHaveText('7 of 100') + await input.fill('') + await expectPagination(page, { pageIndex: 0, pageSize: 10 }) + await expect(pageStatus(page)).toHaveText('1 of 100') + + expect(errors).toEqual([]) +}) + +test('recomputes the page index when the page size changes', async ({ + page, +}) => { + const errors = await openExample(page) + const select = pageSizeSelect(page) + + await expect(select.locator('option')).toHaveText( + PAGE_SIZES.map((size) => `Show ${size}`), + ) + await expect(select).toHaveValue('10') + + await select.selectOption('20') + await expect(bodyRows(page)).toHaveCount(20) + await expect(pageStatus(page)).toHaveText('1 of 50') + await expect(rowCountLine(page)).toHaveText('Showing 20 of 1,000 Rows') + await expectPagination(page, { pageIndex: 0, pageSize: 20 }) + + await select.selectOption('50') + await expect(bodyRows(page)).toHaveCount(50) + await expect(pageStatus(page)).toHaveText('1 of 20') + await expect(rowCountLine(page)).toHaveText('Showing 50 of 1,000 Rows') + + // `setPageSize` keeps the row at the top of the page in view by deriving the + // new page index from the old top row index, rather than resetting to page 1. + await select.selectOption('10') + await goToPageInput(page).fill('5') + await expect(pageStatus(page)).toHaveText('5 of 100') + + // Top row index is 4 * 10 = 40, so 20 rows per page lands on page 3. + await select.selectOption('20') + await expect(pageStatus(page)).toHaveText('3 of 50') + await expectPagination(page, { pageIndex: 2, pageSize: 20 }) + + // Top row index is still 2 * 20 = 40, so 50 rows per page lands on page 1. + await select.selectOption('50') + await expect(pageStatus(page)).toHaveText('1 of 20') + await expectPagination(page, { pageIndex: 0, pageSize: 50 }) + + expect(errors).toEqual([]) +}) + +test('resets to the first page when data is regenerated', async ({ page }) => { + const errors = await openExample(page) + + await goToPageInput(page).fill('5') + await expect(pageStatus(page)).toHaveText('5 of 100') + + // `autoResetPageIndex` defaults to true, so new data returns to page one. + await page.getByRole('button', { name: /^Regenerate Data$/i }).click() + + await expect(pageStatus(page)).toHaveText('1 of 100') + await expectPagination(page, { pageIndex: 0, pageSize: 10 }) + await expect(rowCountLine(page)).toHaveText( + `Showing 10 of ${TOTAL_ROWS.toLocaleString('en-US')} Rows`, + ) + + expect(errors).toEqual([]) +}) diff --git a/examples/octane/pagination/tsconfig.json b/examples/octane/pagination/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/pagination/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/pagination/vite.config.ts b/examples/octane/pagination/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/pagination/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/row-pinning/index.html b/examples/octane/row-pinning/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/row-pinning/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/row-pinning/package.json b/examples/octane/row-pinning/package.json new file mode 100644 index 0000000000..f7a41fd469 --- /dev/null +++ b/examples/octane/row-pinning/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-row-pinning", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/row-pinning/src/index.css b/examples/octane/row-pinning/src/index.css new file mode 100644 index 0000000000..ac32e94c64 --- /dev/null +++ b/examples/octane/row-pinning/src/index.css @@ -0,0 +1,388 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +thead { + background: lightgray; + margin: 0; + position: sticky; + top: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td:last-child { + border-right: 0; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +.container { + border: 1px solid lightgray; + height: 500px; + max-width: 900px !important; + overflow: auto; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/row-pinning/src/main.tsrx b/examples/octane/row-pinning/src/main.tsrx new file mode 100644 index 0000000000..1d471707c7 --- /dev/null +++ b/examples/octane/row-pinning/src/main.tsrx @@ -0,0 +1,441 @@ +import { useMemo, useState } from 'octane' +import { createRoot } from 'octane' +import { + columnFilteringFeature, + columnSizingFeature, + createColumnHelper, + createExpandedRowModel, + createFilteredRowModel, + createPaginatedRowModel, + filterFn_inNumberRange, + filterFn_includesString, + rowExpandingFeature, + rowPaginationFeature, + rowPinningFeature, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { Person } from './makeData' +import type { + Column, + ExpandedState, + OctaneTable, + Row, + RowPinningState, +} from '@tanstack/octane-table' +import './index.css' + +const features = tableFeatures({ + rowPinningFeature, + rowExpandingFeature, + columnFilteringFeature, + columnSizingFeature, + rowPaginationFeature, + filteredRowModel: createFilteredRowModel(), + expandedRowModel: createExpandedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +const columnHelper = createColumnHelper() +function App() @{ + // table states + const [rowPinning, setRowPinning] = useState({ + top: [], + bottom: [], + }) + const [expanded, setExpanded] = useState({}) + + // demo states + const [keepPinnedRows, setKeepPinnedRows] = useState(true) + const [includeLeafRows, setIncludeLeafRows] = useState(true) + const [includeParentRows, setIncludeParentRows] = useState(false) + const [copyPinnedRows, setCopyPinnedRows] = useState(false) + + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.display({ + id: 'pin', + header: () => 'Pin', + cell: ({ row }) => + row.getIsPinned() ? ( + + ) : ( +
+ + +
+ ), + }), + columnHelper.accessor('firstName', { + header: ({ table }) => ( + <> + {' '} + First Name + + ), + cell: ({ row, getValue }) => ( +
+ <> + {row.getCanExpand() ? ( + + ) : ( + '🔵' + )}{' '} + {getValue()} + +
+ ), + footer: (props) => props.column.id, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + }), + columnHelper.accessor('age', { + header: () => 'Age', + size: 50, + }), + columnHelper.accessor('visits', { + header: () => Visits, + size: 50, + }), + columnHelper.accessor('status', { + header: 'Status', + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + size: 80, + }), + ]), + [includeLeafRows, includeParentRows], + ) + + const [data, setData] = useState(() => makeData(1_000, 2, 2)) + const refreshData = () => setData(() => makeData(1_000, 2, 2)) + const stressTest = () => setData(() => makeData(200_000, 2, 2)) + + const table = useTable( + { + features, + columns, + data, + initialState: { pagination: { pageSize: 20, pageIndex: 0 } }, + state: { + expanded, + rowPinning, + }, + onExpandedChange: setExpanded, + onRowPinningChange: setRowPinning, + getSubRows: (row) => row.subRows, + keepPinnedRows, + // atoms: { rowPinning: rowPinningAtom }, // preferred: own pinning state with an external atom + // enableRowPinning: row => row.original.age > 18, // allow pinning only for matching rows; default true + debugTable: true, + debugAll: true, + }, + (state) => state, // default selector + ) + +
+
+ + +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ) + })} + + ))} + + + {table.getTopRows().map((row) => ( + + ))} + {(copyPinnedRows + ? table.getRowModel().rows + : table.getCenterRows() + ).map((row) => { + return ( + + {row.getAllCells().map((cell) => { + return ( + + ) + })} + + ) + })} + {table.getBottomRows().map((row) => ( + + ))} + +
+ {header.isPlaceholder ? null : ( + <> + + {header.column.getCanFilter() ? ( +
+ +
+ ) : null} + + )} +
+ +
+
+ +
+
+ + + + + +
Page
+ + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + { + const page = (e.target as HTMLInputElement).value + ? Number((e.target as HTMLInputElement).value) - 1 + : 0 + table.setPageIndex(page) + }} + className="page-size-input" + /> + + +
+
+
+
+
+
+ setKeepPinnedRows(!keepPinnedRows)} + /> + +
+
+ setIncludeLeafRows(!includeLeafRows)} + /> + +
+
+ setIncludeParentRows(!includeParentRows)} + /> + +
+
+ setCopyPinnedRows(!copyPinnedRows)} + /> + +
+
+
+
+        {JSON.stringify(table.state, null, 2)}
+      
+
+} + +function PinnedRow({ + row, + table, +}: { + row: Row + table: OctaneTable +}) @{ + + {row.getAllCells().map((cell) => { + return ( + + + + ) + })} + +} + +function Filter({ + column, + table, +}: { + column: Column + table: OctaneTable +}) { + const firstValue = table + .getPreFilteredRowModel() + .flatRows[0]?.getValue(column.id) + + return typeof firstValue === 'number' ? ( +
+ + column.setFilterValue((old: any) => [ + (e.target as HTMLInputElement).value, + old?.[1], + ]) + } + placeholder={`Min`} + className="filter-input" + /> + + column.setFilterValue((old: any) => [ + old?.[0], + (e.target as HTMLInputElement).value, + ]) + } + placeholder={`Max`} + className="filter-input" + /> +
+ ) : ( + + column.setFilterValue((e.target as HTMLInputElement).value) + } + placeholder={`Search...`} + className="filter-select" + /> + ) +} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/row-pinning/src/makeData.ts b/examples/octane/row-pinning/src/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/octane/row-pinning/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/row-pinning/src/vite-env.d.ts b/examples/octane/row-pinning/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/row-pinning/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/row-pinning/tests/e2e/smoke.spec.ts b/examples/octane/row-pinning/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..2e1ab83f96 --- /dev/null +++ b/examples/octane/row-pinning/tests/e2e/smoke.spec.ts @@ -0,0 +1,228 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +/** + * `makeData(1_000, 2, 2)` gives each row two children and each of those two + * children, so pinning one row pins a subtree of seven while the include-leaf + * and include-parent options are on. + */ +const SUBTREE_SIZE = 7 + +let server: Awaited> | undefined + +test.beforeAll(async () => { + // One server for the whole file. A cold Vite start costs more than a test. + test.setTimeout(180_000) + server = await startExampleServer(exampleDir) +}) + +test.afterAll(async () => { + await server?.close() +}) + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + if (!server) throw new Error('Example server failed to start') + + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return errors +} + +function bodyRows(page: Page) { + return page.locator('tbody tr') +} + +/** + * The pin controls sit in each row's first cell. An unpinned row shows ⬆️ then + * ⬇️; a pinned row shows a single ❌. Locating by position avoids matching on + * emoji accessible names, which do not survive name normalisation. + */ +function pinControls(page: Page, rowIndex: number) { + return bodyRows(page).nth(rowIndex).locator('td').first().getByRole('button') +} + +function optionCheckbox(page: Page, label: RegExp) { + return page + .locator('.vertical-options > div') + .filter({ hasText: label }) + .locator('input[type="checkbox"]') +} + +function pageButton(page: Page, label: '<<' | '<' | '>' | '>>') { + return page.getByRole('button', { name: label, exact: true }) +} + +/** Row data is random faker output, so `table.state` is the stable oracle. */ +async function readRowPinning(page: Page) { + const text = await page.getByTestId('table-state').textContent() + const state = JSON.parse(text ?? '{}') as { + rowPinning?: { top?: Array; bottom?: Array } + } + + return { + top: state.rowPinning?.top ?? [], + bottom: state.rowPinning?.bottom ?? [], + } +} + +async function getRowText(page: Page, index: number) { + const text = await bodyRows(page).nth(index).textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const errors = await openExample(page) + const table = page.locator('table').first() + + await expect(table).toBeVisible() + await expect(bodyRows(page).first()).toBeVisible() + expect(await readRowPinning(page)).toEqual({ top: [], bottom: [] }) + // An unpinned row offers both pin directions and no unpin control. + await expect(pinControls(page, 0)).toHaveCount(2) + await expect(pinControls(page, 0).nth(0)).toHaveText('⬆️') + await expect(pinControls(page, 0).nth(1)).toHaveText('⬇️') + + expect(errors).toEqual([]) +}) + +test('regenerates table data', async ({ page }) => { + const errors = await openExample(page) + const regenerateButton = page.getByRole('button', { + name: /^Regenerate Data$/i, + }) + + await expect(bodyRows(page).first()).toBeVisible() + + const firstRowBefore = await getRowText(page, 0) + + await regenerateButton.click() + + await expect.poll(() => getRowText(page, 0)).not.toBe(firstRowBefore) + + expect(errors).toEqual([]) +}) + +test('pins a row to the top', async ({ page }) => { + const errors = await openExample(page) + + await pinControls(page, 2).nth(0).click() + + // Include-leaf and include-parent are on by default, so the row arrives with + // its two children and their two children apiece. + await expect + .poll(async () => (await readRowPinning(page)).top) + .toHaveLength(SUBTREE_SIZE) + expect((await readRowPinning(page)).top[0]).toBe('2') + expect((await readRowPinning(page)).bottom).toEqual([]) + + // The pinned row is lifted to the front and swaps its controls for an unpin. + await expect(pinControls(page, 0)).toHaveCount(1) + await expect(pinControls(page, 0)).toHaveText('❌') + + expect(errors).toEqual([]) +}) + +test('pins a row to the bottom', async ({ page }) => { + const errors = await openExample(page) + + await pinControls(page, 1).nth(1).click() + + await expect + .poll(async () => (await readRowPinning(page)).bottom) + .toHaveLength(SUBTREE_SIZE) + expect((await readRowPinning(page)).top).toEqual([]) + + // The pinned subtree now sits at the very end of the table body. + const lastIndex = (await bodyRows(page).count()) - 1 + await expect(pinControls(page, lastIndex)).toHaveText('❌') + await expect(pinControls(page, 0)).toHaveCount(2) + + expect(errors).toEqual([]) +}) + +test('unpins a row', async ({ page }) => { + const errors = await openExample(page) + + await pinControls(page, 2).nth(0).click() + await expect + .poll(async () => (await readRowPinning(page)).top) + .toHaveLength(SUBTREE_SIZE) + + await pinControls(page, 0).click() + + // Unpinning the parent releases the descendants that were pinned with it. + await expect.poll(async () => (await readRowPinning(page)).top).toEqual([]) + await expect(pinControls(page, 0)).toHaveCount(2) + await expect(pinControls(page, 0).nth(0)).toHaveText('⬆️') + await expect(pinControls(page, 0).nth(1)).toHaveText('⬇️') + + expect(errors).toEqual([]) +}) + +test('keeps pinned rows visible across pages', async ({ page }) => { + const errors = await openExample(page) + + await pinControls(page, 2).nth(0).click() + await expect + .poll(async () => (await readRowPinning(page)).top) + .toHaveLength(SUBTREE_SIZE) + + const pinnedText = await getRowText(page, 0) + + // `keepPinnedRows` is on by default, so paging away keeps the row on screen. + await pageButton(page, '>').click() + + await expect.poll(() => getRowText(page, 0)).toBe(pinnedText) + await expect(pinControls(page, 0)).toHaveText('❌') + await expect + .poll(async () => (await readRowPinning(page)).top) + .toHaveLength(SUBTREE_SIZE) + + expect(errors).toEqual([]) +}) + +test('duplicates a pinned row when copying is enabled', async ({ page }) => { + const errors = await openExample(page) + const copyOption = optionCheckbox(page, /Duplicate\/Keep Pinned Rows/) + + await expect(copyOption).not.toBeChecked() + + await pinControls(page, 2).nth(0).click() + await expect + .poll(async () => (await readRowPinning(page)).top) + .toHaveLength(SUBTREE_SIZE) + + const rowsPinnedOnly = await bodyRows(page).count() + + await copyOption.check() + + // With copying on the row keeps its place in the body as well as its pinned + // slot, so the body grows rather than simply reordering. + await expect + .poll(() => bodyRows(page).count()) + .toBeGreaterThan(rowsPinnedOnly) + + expect(errors).toEqual([]) +}) diff --git a/examples/octane/row-pinning/tsconfig.json b/examples/octane/row-pinning/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/row-pinning/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/row-pinning/vite.config.ts b/examples/octane/row-pinning/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/row-pinning/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/row-selection/index.html b/examples/octane/row-selection/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/row-selection/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/row-selection/package.json b/examples/octane/row-selection/package.json new file mode 100644 index 0000000000..ae4bda9f71 --- /dev/null +++ b/examples/octane/row-selection/package.json @@ -0,0 +1,24 @@ +{ + "name": "@tanstack/octane-table-example-row-selection", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-store": "^0.12.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/row-selection/src/index.css b/examples/octane/row-selection/src/index.css new file mode 100644 index 0000000000..0a099f6d9f --- /dev/null +++ b/examples/octane/row-selection/src/index.css @@ -0,0 +1,372 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +button:disabled, +button[disabled] { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/row-selection/src/main.tsrx b/examples/octane/row-selection/src/main.tsrx new file mode 100644 index 0000000000..9d043f51b7 --- /dev/null +++ b/examples/octane/row-selection/src/main.tsrx @@ -0,0 +1,385 @@ +import { useEffect, useMemo, useRef, useState } from 'octane' +import { createRoot } from 'octane' +import { + columnFilteringFeature, + createColumnHelper, + createFilteredRowModel, + createPaginatedRowModel, + filterFn_inNumberRange, + filterFn_includesString, + globalFilteringFeature, + rowPaginationFeature, + rowSelectionFeature, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { useCreateAtom } from '@tanstack/octane-store' +import { makeData } from './makeData' +import type { + Column, + RowSelectionState, + Table, +} from '@tanstack/octane-table' +import type { Person } from './makeData' +import './index.css' + +const features = tableFeatures({ + rowPaginationFeature, + rowSelectionFeature, + columnFilteringFeature, + globalFilteringFeature, + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + filterFns: { + includesString: filterFn_includesString, + inNumberRange: filterFn_inNumberRange, + }, +}) + +const columnHelper = createColumnHelper() + +function App() @{ + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.display({ + id: 'select', + header: () => { + return ( + + ) + }, + cell: ({ row }) => ( +
+ +
+ ), + }), + columnHelper.accessor('firstName', { + header: 'First Name', + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + header: () => Last Name, + cell: (info) => info.getValue(), + footer: (props) => props.column.id, + }), + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + }), + columnHelper.accessor('visits', { + header: () => Visits, + footer: (props) => props.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + }), + ]), + [], + ) + + const [data, setData] = useState(() => makeData(1_000)) + const refreshData = () => setData(() => makeData(1_000)) + const stressTest = () => setData(() => makeData(1_000_000)) + + const rowSelectionAtom = useCreateAtom({}) + + const table = useTable( + { + features, + atoms: { + rowSelection: rowSelectionAtom, + }, + columns, + data, + getRowId: (row) => row.id, + enableRowSelection: true, // enable row selection for all rows + // initialState: { rowSelection: { '0': true } }, // select rows on first render + // state: { rowSelection }, // classic controlled state; pair with onRowSelectionChange + // onRowSelectionChange: setRowSelection, + // enableMultiRowSelection: false, // allow only one selected row at a time; default true + // enableRowRangeSelection: false, // disable Shift-click range selection; default true + // enableSubRowSelection: false, // do not select a parent's subrows with it; default true + // isRowRangeSelectionEvent: event => Boolean(event.metaKey), // use Meta instead of Shift + debugTable: true, + }, + (state) => state, // default selector + ) + + <> +
+
+ + +
+
+ + table.setGlobalFilter((e.target as HTMLInputElement).value) + } + className="summary-panel" + placeholder="Search all columns..." + /> +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + return ( + + {row.getAllCells().map((cell) => { + return ( + + ) + })} + + ) + })} + + + + + + + +
+ {header.isPlaceholder ? null : ( + <> + + {header.column.getCanFilter() ? ( +
+ +
+ ) : null} + + )} +
+ +
+ + + Page Rows ({table.getRowModel().rows.length.toLocaleString()}) +
+
+
+ + + + + +
Page
+ + {(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + { + const page = (e.target as HTMLInputElement).value + ? Number((e.target as HTMLInputElement).value) - 1 + : 0 + table.setPageIndex(page) + }} + className="page-size-input" + /> + + +
+
+
+ <> + {Object.keys(table.state.rowSelection).length.toLocaleString()}{' '} + of{' '} + + {table.getPreFilteredRowModel().rows.length.toLocaleString()} Total + Rows Selected +
+
+
+
+
+ +
+
+ +
+            {JSON.stringify(table.state, null, 2)}
+          
+
+
+ +} + +function Filter({ + column, + table, +}: { + column: Column + table: Table +}) { + const firstValue = table + .getPreFilteredRowModel() + .flatRows[0]?.getValue(column.id) + + return typeof firstValue === 'number' ? ( +
+ + column.setFilterValue((old: any) => [ + (e.target as HTMLInputElement).value, + old?.[1], + ]) + } + placeholder={`Min`} + className="filter-input" + /> + + column.setFilterValue((old: any) => [ + old?.[0], + (e.target as HTMLInputElement).value, + ]) + } + placeholder={`Max`} + className="filter-input" + /> +
+ ) : ( + + column.setFilterValue((e.target as HTMLInputElement).value) + } + placeholder={`Search...`} + className="filter-select" + /> + ) +} + +function IndeterminateCheckbox({ + indeterminate, + className = '', + checked, + onChange, + disabled, + ...rest +}: { + indeterminate?: boolean + checked?: boolean + disabled?: boolean + onChange?: (event: Event) => void +} & Record) @{ + const ref = useRef(null!) + + useEffect(() => { + if (typeof indeterminate === 'boolean') { + ref.current.indeterminate = !checked && indeterminate + } + }, [ref, indeterminate, checked]) + + +} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/row-selection/src/makeData.ts b/examples/octane/row-selection/src/makeData.ts new file mode 100644 index 0000000000..c34c43a03e --- /dev/null +++ b/examples/octane/row-selection/src/makeData.ts @@ -0,0 +1,50 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + id: string + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + id: faker.string.uuid(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/row-selection/src/vite-env.d.ts b/examples/octane/row-selection/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/row-selection/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/row-selection/tests/e2e/smoke.spec.ts b/examples/octane/row-selection/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..2b97c8df0a --- /dev/null +++ b/examples/octane/row-selection/tests/e2e/smoke.spec.ts @@ -0,0 +1,301 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Locator, Page } from '@playwright/test' + +const exampleDir = path.resolve() + +/** `makeData(1_000)` with a uuid `getRowId`, ten rows to a page. */ +const TOTAL_ROWS = '1,000' +const PAGE_SIZE = 10 + +let server: Awaited> | undefined + +test.beforeAll(async () => { + // One server for the whole file. A cold Vite start costs more than a test. + test.setTimeout(180_000) + server = await startExampleServer(exampleDir) +}) + +test.afterAll(async () => { + await server?.close() +}) + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + if (!server) throw new Error('Example server failed to start') + + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return errors +} + +function rowCheckboxes(page: Page) { + return page.locator('tbody tr td:first-child input[type="checkbox"]') +} + +/** Bound to `getIsAllRowsSelected`, so it spans every page. */ +function headerCheckbox(page: Page) { + return page.locator('thead th').first().locator('input[type="checkbox"]') +} + +/** Bound to `getIsAllPageRowsSelected`, so it spans the current page only. */ +function footerCheckbox(page: Page) { + return page.locator('tfoot input[type="checkbox"]') +} + +/** Anchored so the regex cannot also match an ancestor with extra text. */ +function selectionSummary(page: Page) { + return page.locator('div').filter({ + hasText: /^\s*[\d,]+\s+of\s+[\d,]+\s+Total\s+Rows\s+Selected\s*$/, + }) +} + +function pageRowsLabel(page: Page) { + return page.locator('tfoot td').nth(1) +} + +function globalFilterInput(page: Page) { + return page.getByPlaceholder('Search all columns...') +} + +function pageButton(page: Page, label: '<<' | '<' | '>' | '>>') { + return page.getByRole('button', { name: label, exact: true }) +} + +function isIndeterminate(checkbox: Locator) { + return checkbox.evaluate((el) => (el as HTMLInputElement).indeterminate) +} + +async function expectIndeterminate(checkbox: Locator, expected: boolean) { + await expect.poll(() => isIndeterminate(checkbox)).toBe(expected) +} + +/** Row ids are uuids, so only the number of selected keys is assertable. */ +async function readSelectionCount(page: Page) { + const text = await page.getByTestId('table-state').textContent() + const state = JSON.parse(text ?? '{}') as { + rowSelection?: Record + } + + return Object.keys(state.rowSelection ?? {}).length +} + +async function expectSelectionCount(page: Page, expected: number) { + await expect.poll(() => readSelectionCount(page)).toBe(expected) + await expect(selectionSummary(page)).toHaveText( + `${expected.toLocaleString('en-US')} of ${TOTAL_ROWS} Total Rows Selected`, + ) +} + +async function readGlobalFilter(page: Page) { + const text = await page.getByTestId('table-state').textContent() + const state = JSON.parse(text ?? '{}') as { globalFilter?: string } + + return state.globalFilter ?? '' +} + +async function getFirstBodyRowText(page: Page) { + const text = await page.locator('tbody tr').first().textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const errors = await openExample(page) + const table = page.locator('table').first() + + await expect(table).toBeVisible() + await expect(page.locator('tbody tr')).toHaveCount(PAGE_SIZE) + await expect(pageRowsLabel(page)).toHaveText(`Page Rows (${PAGE_SIZE})`) + await expectSelectionCount(page, 0) + await expect(headerCheckbox(page)).not.toBeChecked() + await expectIndeterminate(headerCheckbox(page), false) + + expect(errors).toEqual([]) +}) + +test('regenerates table data', async ({ page }) => { + const errors = await openExample(page) + const regenerateButton = page.getByRole('button', { + name: /^Regenerate Data$/i, + }) + + await expect(page.locator('tbody tr').first()).toBeVisible() + + const firstRowBefore = await getFirstBodyRowText(page) + + await regenerateButton.click() + + await expect.poll(() => getFirstBodyRowText(page)).not.toBe(firstRowBefore) + await expect(page.locator('tbody tr')).toHaveCount(PAGE_SIZE) + + expect(errors).toEqual([]) +}) + +test('selects and deselects a single row', async ({ page }) => { + const errors = await openExample(page) + const firstRow = rowCheckboxes(page).first() + + await firstRow.check() + + await expect(firstRow).toBeChecked() + await expectSelectionCount(page, 1) + // One of many rows selected leaves both select-all boxes partially filled. + await expectIndeterminate(headerCheckbox(page), true) + await expectIndeterminate(footerCheckbox(page), true) + await expect(headerCheckbox(page)).not.toBeChecked() + + await firstRow.uncheck() + + await expect(firstRow).not.toBeChecked() + await expectSelectionCount(page, 0) + await expectIndeterminate(headerCheckbox(page), false) + await expectIndeterminate(footerCheckbox(page), false) + + expect(errors).toEqual([]) +}) + +test('selects an inclusive range with Shift-click', async ({ page }) => { + const errors = await openExample(page) + const checkboxes = rowCheckboxes(page) + + await checkboxes.nth(1).check() + await expectSelectionCount(page, 1) + + // The range covers both endpoints, so rows 1 through 5 is five rows. + await checkboxes.nth(5).click({ modifiers: ['Shift'] }) + + await expectSelectionCount(page, 5) + + for (const index of [1, 2, 3, 4, 5]) { + await expect(checkboxes.nth(index)).toBeChecked() + } + + // Rows outside the range are untouched. + await expect(checkboxes.nth(0)).not.toBeChecked() + await expect(checkboxes.nth(6)).not.toBeChecked() + + expect(errors).toEqual([]) +}) + +test('selects every row on the current page from the footer', async ({ + page, +}) => { + const errors = await openExample(page) + + await footerCheckbox(page).check() + + await expectSelectionCount(page, PAGE_SIZE) + await expect(footerCheckbox(page)).toBeChecked() + await expectIndeterminate(footerCheckbox(page), false) + // Ten of a thousand rows is still only a partial selection overall. + await expect(headerCheckbox(page)).not.toBeChecked() + await expectIndeterminate(headerCheckbox(page), true) + + for (let index = 0; index < PAGE_SIZE; index++) { + await expect(rowCheckboxes(page).nth(index)).toBeChecked() + } + + // Clearing a single row drops the footer back to a partial state. + await rowCheckboxes(page).nth(3).uncheck() + await expectSelectionCount(page, PAGE_SIZE - 1) + await expect(footerCheckbox(page)).not.toBeChecked() + await expectIndeterminate(footerCheckbox(page), true) + + await footerCheckbox(page).check() + await expectSelectionCount(page, PAGE_SIZE) + + expect(errors).toEqual([]) +}) + +test('selects every row across all pages from the header', async ({ page }) => { + const errors = await openExample(page) + + await headerCheckbox(page).check() + + await expectSelectionCount(page, 1_000) + await expect(headerCheckbox(page)).toBeChecked() + await expectIndeterminate(headerCheckbox(page), false) + await expect(footerCheckbox(page)).toBeChecked() + + // Selection is keyed by row id rather than position, so the next page's rows + // come back already selected. + await pageButton(page, '>').click() + + await expect(footerCheckbox(page)).toBeChecked() + await expectSelectionCount(page, 1_000) + + for (let index = 0; index < PAGE_SIZE; index++) { + await expect(rowCheckboxes(page).nth(index)).toBeChecked() + } + + await headerCheckbox(page).uncheck() + await expectSelectionCount(page, 0) + + expect(errors).toEqual([]) +}) + +test('keeps selection when the global filter narrows the rows', async ({ + page, +}) => { + const errors = await openExample(page) + + await rowCheckboxes(page).first().check() + await expectSelectionCount(page, 1) + + // The first name of the row that was just selected, so it survives the + // filter. The input debounces, so poll the state rather than sleeping. + const firstName = + ( + await page.locator('tbody tr').first().locator('td').nth(1).textContent() + )?.trim() ?? '' + + await globalFilterInput(page).fill(firstName) + + await expect.poll(() => readGlobalFilter(page)).toBe(firstName) + await expect(page.locator('tbody tr').first()).toBeVisible() + // The summary counts pre-filtered rows, so the denominator does not move. + await expectSelectionCount(page, 1) + + await globalFilterInput(page).fill('') + + await expect.poll(() => readGlobalFilter(page)).toBe('') + await expectSelectionCount(page, 1) + + expect(errors).toEqual([]) +}) + +test('tracks the page size in the footer row count', async ({ page }) => { + const errors = await openExample(page) + const select = page.locator('.controls select') + + await expect(pageRowsLabel(page)).toHaveText('Page Rows (10)') + + await select.selectOption('20') + + await expect(page.locator('tbody tr')).toHaveCount(20) + await expect(pageRowsLabel(page)).toHaveText('Page Rows (20)') + + // Selecting the page now covers twenty rows rather than ten. + await footerCheckbox(page).check() + await expectSelectionCount(page, 20) + + expect(errors).toEqual([]) +}) diff --git a/examples/octane/row-selection/tsconfig.json b/examples/octane/row-selection/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/row-selection/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/row-selection/vite.config.ts b/examples/octane/row-selection/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/row-selection/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/sorting/.gitignore b/examples/octane/sorting/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/examples/octane/sorting/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/octane/sorting/index.html b/examples/octane/sorting/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/sorting/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/sorting/package.json b/examples/octane/sorting/package.json new file mode 100644 index 0000000000..7992fda2b4 --- /dev/null +++ b/examples/octane/sorting/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-sorting", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/sorting/src/index.css b/examples/octane/sorting/src/index.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/octane/sorting/src/index.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/sorting/src/main.tsrx b/examples/octane/sorting/src/main.tsrx new file mode 100644 index 0000000000..767cc08b81 --- /dev/null +++ b/examples/octane/sorting/src/main.tsrx @@ -0,0 +1,197 @@ +import { createRoot } from 'octane' +import { useMemo, useState } from 'octane' +import { + createColumnHelper, + createSortedRowModel, + rowSortingFeature, + sortFn_alphanumeric, + sortFn_datetime, + sortFn_text, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { SortFn, SortingState } from '@tanstack/octane-table' +import type { Person } from './makeData' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns: { + alphanumeric: sortFn_alphanumeric, + datetime: sortFn_datetime, + text: sortFn_text, + }, +}) + +const columnHelper = createColumnHelper() +// custom sorting logic for one of our enum columns +const sortStatusFn: SortFn = ( + rowA, + rowB, + _columnId, +) => { + const statusA = rowA.original.status + const statusB = rowB.original.status + const statusOrder = ['single', 'complicated', 'relationship'] + return statusOrder.indexOf(statusA) - statusOrder.indexOf(statusB) +} + +function App() @{ + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.display({ + id: 'rowNumber', + header: '#', + cell: ({ row }) => row.getDisplayIndex() + 1, + }), + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + sortUndefined: 'last', + sortDescFirst: false, + }), + columnHelper.accessor('email', { + header: 'Email', + sortFn: 'alphanumeric', + }), + columnHelper.accessor('age', { + header: () => 'Age', + }), + columnHelper.accessor('visits', { + header: () => Visits, + sortUndefined: 'last', + }), + columnHelper.accessor('status', { + header: 'Status', + sortFn: sortStatusFn, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + }), + columnHelper.accessor('rank', { + header: 'Rank', + invertSorting: true, + }), + columnHelper.accessor('createdAt', { + header: 'Created At', + }), + ]), + [], + ) + + const [data, setData] = useState(() => makeData(1_000)) + const refreshData = () => setData(() => makeData(1_000)) + const stressTest = () => setData(() => makeData(1_000_000)) + + // optionally, manage sorting state in your own state management (although octane state causes more re-renders here than necessary) + const [sorting, setSorting] = useState([]) + + console.log('sorting', sorting) + + const table = useTable( + { + features, + columns, + data, + state: { + sorting, + }, + onSortingChange: setSorting, + // initialState: { sorting: [{ id: 'firstName', desc: false }] }, // set the initial sort once + // atoms: { sorting: sortingAtom }, // preferred: own sorting state with an external atom + // enableSorting: false, // disable sorting for every column; default true + // sortDescFirst: true, // start every sort cycle with descending order; inferred by column data by default + // enableSortingRemoval: false, // keep a sorted column sorted when toggling; default true + // enableMultiSort: false, // disable Shift-click multi-sorting; default true + // enableMultiRemove: false, // prevent a multi-sort toggle from removing a sorted column; default true + // isMultiSortEvent: () => true, // make every sort interaction a multi-sort; default requires Shift + // maxMultiSortColCount: 3, // limit multi-sorting to three columns; default Infinity + // manualSorting: true, // pass data that is already sorted, for example from a server + // autoResetPageIndex: false, // with pagination, keep the current page when sorting changes; default true + debugTable: true, + }, + (state) => state, // default selector + ) + +
+
+ + +
+ <> +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ) + })} + + ))} + + + {table + .getRowModel() + .rows.slice(0, 10) + .map((row) => { + return ( + + {row.getAllCells().map((cell) => { + return ( + + ) + })} + + ) + })} + +
+ {header.isPlaceholder ? null : ( +
+ + {{ + asc: ' 🔼', + desc: ' 🔽', + }[header.column.getIsSorted() as string] ?? null} +
+ )} +
+ +
+
{String(table.getRowModel().rows.length.toLocaleString())} Rows
+
+ {/* Store mode: full state for debugging */} +
+          {JSON.stringify(table.state, null, 2)}
+        
+ +
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/sorting/src/makeData.ts b/examples/octane/sorting/src/makeData.ts new file mode 100644 index 0000000000..b57918f94c --- /dev/null +++ b/examples/octane/sorting/src/makeData.ts @@ -0,0 +1,60 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string | undefined + email: string + age: number + visits: number | undefined + progress: number + status: 'relationship' | 'complicated' | 'single' + rank: number + createdAt: Date + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + const firstName = faker.person.firstName() + const lastName = faker.person.lastName() + + return { + firstName, + lastName: Math.random() < 0.1 ? undefined : lastName, + email: faker.internet + .email({ firstName, lastName }) + .replace('@', `${faker.number.int(9999)}@`) + .toLowerCase(), + age: faker.number.int(40), + visits: Math.random() < 0.1 ? undefined : faker.number.int(1000), + progress: faker.number.int(100), + createdAt: faker.date.anytime(), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + rank: faker.number.int(100), + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/sorting/src/vite-env.d.ts b/examples/octane/sorting/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/sorting/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/sorting/tests/e2e/smoke.spec.ts b/examples/octane/sorting/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..ebcaf4851c --- /dev/null +++ b/examples/octane/sorting/tests/e2e/smoke.spec.ts @@ -0,0 +1,339 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +/** Body cell index per column, matching the column order in `src/main.tsrx`. */ +const COLUMN = { + rowNumber: 0, + firstName: 1, + lastName: 2, + email: 3, + age: 4, + visits: 5, + status: 6, + progress: 7, + rank: 8, + createdAt: 9, +} as const + +type ColumnName = keyof typeof COLUMN + +/** `sortStatusFn` in `src/main.tsrx` orders statuses this way, not alphabetically. */ +const STATUS_ORDER = ['single', 'complicated', 'relationship'] + +let server: Awaited> | undefined + +test.beforeAll(async () => { + // One server for the whole file. A cold Vite start costs more than a test. + test.setTimeout(180_000) + server = await startExampleServer(exampleDir) +}) + +test.afterAll(async () => { + await server?.close() +}) + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + if (!server) throw new Error('Example server failed to start') + + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return errors +} + +/** The clickable header, which also carries the title and the sort glyph. */ +function sortToggle(page: Page, column: ColumnName) { + return page + .locator('thead th') + .nth(COLUMN[column]) + .locator('.sortable-header') +} + +/** Visible body cell text for one column, in render order. */ +async function readColumn(page: Page, column: ColumnName) { + const cells = await page + .locator(`tbody tr td:nth-child(${COLUMN[column] + 1})`) + .allTextContents() + + return cells.map((cell) => cell.trim()) +} + +async function readNumericColumn(page: Page, column: ColumnName) { + return (await readColumn(page, column)).map(Number) +} + +/** Row data is random faker output, so `table.state` is the stable oracle. */ +async function readSorting(page: Page) { + const text = await page.getByTestId('table-state').textContent() + const state = JSON.parse(text ?? '{}') as { + sorting?: Array<{ id: string; desc: boolean }> + } + + return state.sorting ?? [] +} + +async function expectSorting( + page: Page, + expected: Array<{ id: string; desc: boolean }>, +) { + await expect.poll(() => readSorting(page)).toEqual(expected) +} + +async function getFirstBodyRowText(page: Page) { + const text = await page.locator('tbody tr').first().textContent() + return text?.replace(/\s+/g, ' ').trim() ?? '' +} + +test('renders the table without crashing', async ({ page }) => { + const errors = await openExample(page) + const table = page.locator('table').first() + + await expect(table).toBeVisible() + await expect(table.locator('thead th')).toHaveCount(10) + // The example renders 1,000 rows but only slices the first ten into the body. + await expect(table.locator('tbody tr')).toHaveCount(10) + await expect(page.getByText('1,000 Rows')).toBeVisible() + await expectSorting(page, []) + + expect(errors).toEqual([]) +}) + +test('regenerates table data', async ({ page }) => { + const errors = await openExample(page) + const regenerateButton = page.getByRole('button', { + name: /^Regenerate Data$/i, + }) + + await expect(page.locator('tbody tr').first()).toBeVisible() + + const firstRowBefore = await getFirstBodyRowText(page) + + await regenerateButton.click() + + await expect.poll(() => getFirstBodyRowText(page)).not.toBe(firstRowBefore) + await expect(page.locator('tbody tr')).toHaveCount(10) + + expect(errors).toEqual([]) +}) + +test('infers the first sort direction from the column value type', async ({ + page, +}) => { + const errors = await openExample(page) + + // String columns start ascending, every other value type starts descending. + await expect(sortToggle(page, 'firstName')).toHaveAttribute( + 'title', + 'Sort ascending', + ) + await expect(sortToggle(page, 'status')).toHaveAttribute( + 'title', + 'Sort ascending', + ) + await expect(sortToggle(page, 'age')).toHaveAttribute( + 'title', + 'Sort descending', + ) + await expect(sortToggle(page, 'createdAt')).toHaveAttribute( + 'title', + 'Sort descending', + ) + + // `sortDescFirst: false` overrides the inference on a nullable string column, + // where the first row's value may be undefined and defeat auto detection. + await expect(sortToggle(page, 'lastName')).toHaveAttribute( + 'title', + 'Sort ascending', + ) + + // The row number column has no accessor, so it cannot sort. + await expect(sortToggle(page, 'rowNumber')).toHaveCount(0) + + expect(errors).toEqual([]) +}) + +test('cycles a string column ascending, descending, then clears', async ({ + page, +}) => { + const errors = await openExample(page) + const toggle = sortToggle(page, 'firstName') + + await toggle.click() + await expectSorting(page, [{ id: 'firstName', desc: false }]) + await expect(toggle).toContainText('🔼') + await expect(toggle).toHaveAttribute('title', 'Sort descending') + + await toggle.click() + await expectSorting(page, [{ id: 'firstName', desc: true }]) + await expect(toggle).toContainText('🔽') + await expect(toggle).toHaveAttribute('title', 'Clear sort') + + await toggle.click() + await expectSorting(page, []) + await expect(toggle).not.toContainText('🔼') + await expect(toggle).not.toContainText('🔽') + await expect(toggle).toHaveAttribute('title', 'Sort ascending') + + expect(errors).toEqual([]) +}) + +test('starts a numeric column descending', async ({ page }) => { + const errors = await openExample(page) + const toggle = sortToggle(page, 'age') + + await toggle.click() + await expectSorting(page, [{ id: 'age', desc: true }]) + await expect(toggle).toContainText('🔽') + + await toggle.click() + await expectSorting(page, [{ id: 'age', desc: false }]) + await expect(toggle).toContainText('🔼') + + await toggle.click() + await expectSorting(page, []) + + expect(errors).toEqual([]) +}) + +test('reorders rows to match the sorted column', async ({ page }) => { + const errors = await openExample(page) + const toggle = sortToggle(page, 'age') + const unsorted = await readNumericColumn(page, 'age') + + await toggle.click() + await expect(toggle).toContainText('🔽') + + const descending = await readNumericColumn(page, 'age') + expect(descending).toEqual([...descending].sort((a, b) => b - a)) + // The top row now holds the maximum across all 1,000 rows, which is at least + // the maximum of the ten that happened to be visible before sorting. + expect(descending[0]).toBeGreaterThanOrEqual(Math.max(...unsorted)) + + await toggle.click() + await expect(toggle).toContainText('🔼') + + const ascending = await readNumericColumn(page, 'age') + expect(ascending).toEqual([...ascending].sort((a, b) => a - b)) + expect(ascending[0]).toBeLessThanOrEqual(Math.min(...unsorted)) + + expect(errors).toEqual([]) +}) + +test('inverts the rendered order on the Rank column', async ({ page }) => { + const errors = await openExample(page) + const toggle = sortToggle(page, 'rank') + const unsorted = await readNumericColumn(page, 'rank') + + await toggle.click() + await expectSorting(page, [{ id: 'rank', desc: true }]) + await expect(toggle).toContainText('🔽') + + // `invertSorting: true` flips the comparison after the descending flip, so a + // column marked descending renders ascending values. Golf scores, not typos. + const ranks = await readNumericColumn(page, 'rank') + expect(ranks).toEqual([...ranks].sort((a, b) => a - b)) + expect(ranks[0]).toBeLessThanOrEqual(Math.min(...unsorted)) + + expect(errors).toEqual([]) +}) + +test('keeps undefined values last in both sort directions', async ({ + page, +}) => { + const errors = await openExample(page) + const toggle = sortToggle(page, 'lastName') + + // Roughly a tenth of the 1,000 rows have an undefined last name. The default + // `sortUndefined` would surface them at one end; `'last'` pins them to the + // bottom regardless of direction, so neither direction shows a blank cell. + await toggle.click() + await expect(toggle).toContainText('🔼') + + const ascending = await readColumn(page, 'lastName') + expect(ascending).toHaveLength(10) + expect(ascending.filter((name) => name === '')).toEqual([]) + + await toggle.click() + await expect(toggle).toContainText('🔽') + + const descending = await readColumn(page, 'lastName') + expect(descending).toHaveLength(10) + expect(descending.filter((name) => name === '')).toEqual([]) + + expect(errors).toEqual([]) +}) + +test('sorts by a custom sort function', async ({ page }) => { + const errors = await openExample(page) + const toggle = sortToggle(page, 'status') + + // Ascending puts `single` first. An alphanumeric fallback would put + // `complicated` there instead, so this fails if `sortStatusFn` is dropped. + await toggle.click() + await expectSorting(page, [{ id: 'status', desc: false }]) + await expect(toggle).toContainText('🔼') + + const ascending = await readColumn(page, 'status') + expect([...new Set(ascending)]).toEqual([STATUS_ORDER[0]]) + + await toggle.click() + await expect(toggle).toContainText('🔽') + + const descending = await readColumn(page, 'status') + expect([...new Set(descending)]).toEqual([STATUS_ORDER[2]]) + + expect(errors).toEqual([]) +}) + +test('multi-sorts with Shift-click', async ({ page }) => { + const errors = await openExample(page) + + await sortToggle(page, 'status').click() + await expectSorting(page, [{ id: 'status', desc: false }]) + + // Shift appends a column rather than replacing the sort. + await sortToggle(page, 'age').click({ modifiers: ['Shift'] }) + await expectSorting(page, [ + { id: 'status', desc: false }, + { id: 'age', desc: true }, + ]) + await expect(sortToggle(page, 'status')).toContainText('🔼') + await expect(sortToggle(page, 'age')).toContainText('🔽') + + // A shifted column runs its own asc/desc/remove cycle. + await sortToggle(page, 'age').click({ modifiers: ['Shift'] }) + await expectSorting(page, [ + { id: 'status', desc: false }, + { id: 'age', desc: false }, + ]) + + await sortToggle(page, 'age').click({ modifiers: ['Shift'] }) + await expectSorting(page, [{ id: 'status', desc: false }]) + + // An unshifted click on another column replaces the whole sort. + await sortToggle(page, 'firstName').click() + await expectSorting(page, [{ id: 'firstName', desc: false }]) + + expect(errors).toEqual([]) +}) diff --git a/examples/octane/sorting/tsconfig.json b/examples/octane/sorting/tsconfig.json new file mode 100644 index 0000000000..a33b01356c --- /dev/null +++ b/examples/octane/sorting/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/octane/sorting/vite.config.ts b/examples/octane/sorting/vite.config.ts new file mode 100644 index 0000000000..1317fe0821 --- /dev/null +++ b/examples/octane/sorting/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite' +import { octane } from 'octane/compiler/vite' + +export default defineConfig({ + server: { + port: 7777, + }, + plugins: [octane()], +}) diff --git a/examples/octane/sub-components/index.html b/examples/octane/sub-components/index.html new file mode 100644 index 0000000000..3311ca006a --- /dev/null +++ b/examples/octane/sub-components/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Preact + + +
+ + + diff --git a/examples/octane/sub-components/package.json b/examples/octane/sub-components/package.json new file mode 100644 index 0000000000..9da78c1442 --- /dev/null +++ b/examples/octane/sub-components/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/octane-table-example-sub-components", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src --ext .ts", + "test:e2e": "PLAYWRIGHT_TEST_DIR=$PWD/tests/e2e playwright test --config ../../../playwright.config.ts", + "test:types": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@faker-js/faker": "^10.5.0", + "@tanstack/octane-table": "^9.0.0-beta.65", + "octane": "0.1.21" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "typescript": "6.0.3", + "vite": "^8.1.0" + } +} diff --git a/examples/octane/sub-components/src/index.css b/examples/octane/sub-components/src/index.css new file mode 100644 index 0000000000..b9af6ded32 --- /dev/null +++ b/examples/octane/sub-components/src/index.css @@ -0,0 +1,365 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; + border-collapse: collapse; + border-spacing: 0; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} + +/* Demo layout utilities for plain example styling. */ +.demo-root { + padding: 0.5rem; +} + +.spacer-xs { + height: 0.25rem; +} + +.spacer-sm { + height: 0.5rem; +} + +.spacer-md { + height: 1rem; +} + +.controls, +.button-row, +.inline-controls, +.pin-actions, +.filter-row, +.form-actions { + display: flex; + align-items: center; +} + +.button-row { + flex-wrap: wrap; + gap: 0.5rem; +} + +.controls { + gap: 0.5rem; +} + +.inline-controls, +.pin-actions { + gap: 0.25rem; +} + +.pin-actions { + justify-content: center; +} + +.filter-row { + gap: 0.5rem; +} + +.form-actions { + gap: 1rem; + margin-bottom: 1rem; +} + +.split-tables { + display: flex; + gap: 1rem; +} + +.table-row-group { + display: flex; + border-collapse: collapse; + border-spacing: 0; +} + +.vertical-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.column-toggle-panel { + display: inline-block; + border: 1px solid #000; + border-radius: 0.25rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.column-toggle-panel-header { + border-bottom: 1px solid #000; + padding: 0 0.25rem; +} + +.column-toggle-row, +.selection-cell { + padding: 0 0.25rem; +} + +.selection-cell { + display: block; +} + +.demo-button, +.pin-button, +.compact-input, +.filter-input, +.filter-select, +.page-size-input, +.text-input, +.number-input, +.wide-action-button, +.primary-action, +.secondary-action, +.success-action { + border: 1px solid currentColor; + border-radius: 0.25rem; +} + +.demo-button { + padding: 0.5rem; +} + +.demo-button-sm { + padding: 0.25rem; +} + +.demo-button-spaced { + margin-bottom: 0.5rem; +} + +.pin-button { + padding: 0 0.5rem; +} + +.outlined-table { + border: 2px solid #000; + border-collapse: collapse; + border-spacing: 0; +} + +.outlined-control { + border-color: #000; +} + +.nowrap { + white-space: nowrap; +} + +.demo-note { + margin-bottom: 0.5rem; + font-size: 0.875rem; +} + +.section-title { + font-size: 1.25rem; +} + +.scroll-container { + overflow-x: auto; +} + +.page-size-input { + width: 4rem; + padding: 0.25rem; +} + +.number-input { + width: 5rem; + padding: 0 0.25rem; +} + +.filter-input, +.filter-select { + width: 6rem; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); +} + +.filter-select { + width: 9rem; +} + +.text-input { + width: 100%; + padding: 0 0.25rem; +} + +.compact-input { + padding: 0 0.25rem; +} + +.wide-action-button { + width: 16rem; +} + +.summary-panel { + border: 1px solid currentColor; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.2); + padding: 0.5rem; +} + +.sortable-header, +.sortable { + cursor: pointer; + user-select: none; +} + +.primary-action, +.success-action, +.secondary-action { + color: #fff; +} + +.primary-action { + background: #3b82f6; +} + +.success-action { + background: #22c55e; +} + +.secondary-action { + background: #6b7280; +} + +.submit-button:disabled { + opacity: 0.5; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; +} + +.success-text { + color: #16a34a; +} + +.warning-text { + color: #ca8a04; +} + +.muted-text { + color: #9ca3af; +} + +.label-offset { + margin-left: 0.5rem; +} + +.cell-padding { + padding: 0.25rem; +} + +.table-spacer { + margin-bottom: 0.5rem; + border-collapse: collapse; + border-spacing: 0; +} + +.warning-panel { + margin-bottom: 1rem; + border: 1px solid #facc15; + border-radius: 0.25rem; + background: #fef9c3; + padding: 0.5rem; +} + +.code-block { + overflow: auto; + border-radius: 0.25rem; + background: #f3f4f6; + padding: 0.5rem; + font-size: 0.75rem; +} + +.router-root { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +.page-title { + margin-bottom: 0.25rem; + font-size: 1.5rem; + font-weight: 600; +} + +.disabled-button:disabled { + color: #6b7280; + cursor: not-allowed; +} + +.pagination-controls { + margin-block: 0.5rem; +} + +.column-size-input { + width: 6rem; + margin-left: 0.5rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + padding: 0.25rem; +} + +.form-status { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.centered-button-row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; +} + +.split-gap { + gap: 1rem; +} + +.demo-link { + color: #2563eb; + text-decoration: underline; +} + +.capitalized-text { + text-transform: capitalize; +} + +.centered-text { + text-align: center; +} + +.centered-strong-text { + text-align: center; + font-weight: 600; +} + +.virtualized-title { + text-align: center; + font-size: 1.875rem; + font-weight: 700; +} diff --git a/examples/octane/sub-components/src/main.tsrx b/examples/octane/sub-components/src/main.tsrx new file mode 100644 index 0000000000..d53fef8bba --- /dev/null +++ b/examples/octane/sub-components/src/main.tsrx @@ -0,0 +1,188 @@ +import { useState } from 'octane' +import { createRoot, Fragment } from 'octane' +import './index.css' +import { + createColumnHelper, + createExpandedRowModel, + rowExpandingFeature, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { makeData } from './makeData' +import type { + ColumnDef, + Row, + RowData, + TableFeatures, +} from '@tanstack/octane-table' +import type { Person } from './makeData' +import type { JSX } from 'octane/jsx-runtime' + +const features = tableFeatures({ + rowExpandingFeature, + expandedRowModel: createExpandedRowModel(), +}) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.display({ + id: 'expander', + header: () => null, + cell: ({ row }) => { + return row.getCanExpand() ? ( + + ) : ( + '🔵' + ) + }, + }), + columnHelper.accessor('firstName', { + header: 'First Name', + cell: ({ row, getValue }) => ( +
+ {getValue()} +
+ ), + footer: (props) => props.column.id, + }), + columnHelper.accessor((row) => row.lastName, { + id: 'lastName', + cell: (info) => info.getValue(), + header: () => Last Name, + footer: (props) => props.column.id, + }), + columnHelper.accessor('age', { + header: () => 'Age', + footer: (props) => props.column.id, + }), + columnHelper.accessor('visits', { + header: () => Visits, + footer: (props) => props.column.id, + }), + columnHelper.accessor('status', { + header: 'Status', + footer: (props) => props.column.id, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + footer: (props) => props.column.id, + }), +]) + +type TableProps = { + data: Array + columns: Array> + renderSubComponent: (props: { row: Row }) => JSX.Element + getRowCanExpand: (row: Row) => boolean +} + +function Table({ + columns, + data, + getRowCanExpand, + renderSubComponent, +}: TableProps): JSX.Element @{ + const table = useTable( + { + debugTable: true, + features, + columns, + data, + getRowCanExpand, + }, + (state) => state, // default selector + ) + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + return ( + + + {/* first row is a normal row */} + {row.getAllCells().map((cell) => { + return ( + + ) + })} + + {row.getIsExpanded() && ( + + {/* 2nd row is a custom 1 cell row */} + + + )} + + ) + })} + +
+ {header.isPlaceholder ? null : ( +
+ +
+ )} +
+ +
+ {renderSubComponent({ row })} +
+
+
{String(table.getRowModel().rows.length.toLocaleString())} Rows
+
+} + +const renderSubComponent = ({ row }: { row: Row }) => { + return ( +
+      {JSON.stringify(row.original, null, 2)}
+    
+ ) +} + +function App() @{ + const [data, setData] = useState(() => makeData(10)) + const refreshData = () => setData(makeData(10)) + const stressTest = () => setData(makeData(1_000)) + + <> +
+ + +
+ true} + renderSubComponent={renderSubComponent} + /> + +} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +createRoot(rootElement).render(App) diff --git a/examples/octane/sub-components/src/makeData.ts b/examples/octane/sub-components/src/makeData.ts new file mode 100644 index 0000000000..b9055b2d8c --- /dev/null +++ b/examples/octane/sub-components/src/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Array +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(...lens: Array) { + const makeDataLevel = (depth = 0): Array => { + const len = lens[depth] + return range(len).map((_d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/octane/sub-components/src/vite-env.d.ts b/examples/octane/sub-components/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/octane/sub-components/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/octane/sub-components/tests/e2e/smoke.spec.ts b/examples/octane/sub-components/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000000..d7e5f85479 --- /dev/null +++ b/examples/octane/sub-components/tests/e2e/smoke.spec.ts @@ -0,0 +1,168 @@ +import path from 'node:path' +import { expect, test } from '@playwright/test' +import { startExampleServer } from '../../../../../tests/e2e/helpers/startExampleServer' +import type { Page } from '@playwright/test' + +const exampleDir = path.resolve() + +const TOTAL_ROWS = 10 + +let server: Awaited> | undefined + +test.beforeAll(async () => { + // One server for the whole file. A cold Vite start costs more than a test. + test.setTimeout(180_000) + server = await startExampleServer(exampleDir) +}) + +test.afterAll(async () => { + await server?.close() +}) + +function collectPageErrors(page: Page) { + const errors: Array = [] + + page.on('pageerror', (error) => { + errors.push(error.message) + }) + + page.on('console', (message) => { + if (message.type() === 'error') { + errors.push(message.text()) + } + }) + + return errors +} + +async function openExample(page: Page) { + if (!server) throw new Error('Example server failed to start') + + const errors = collectPageErrors(page) + + await page.goto(server.url) + + return errors +} + +function bodyRows(page: Page) { + return page.locator('tbody tr') +} + +/** Every data row carries an expander in its first cell. */ +function rowExpander(page: Page, index: number) { + return bodyRows(page).nth(index).getByRole('button') +} + +/** The sub component renders the row's own data as JSON inside a `
`. */
+function subComponents(page: Page) {
+  return page.locator('tbody pre')
+}
+
+async function readSubComponentJson(page: Page, index: number) {
+  const text = await subComponents(page).nth(index).textContent()
+  return JSON.parse(text ?? '{}') as Record
+}
+
+async function getFirstBodyRowText(page: Page) {
+  const text = await bodyRows(page).first().textContent()
+  return text?.replace(/\s+/g, ' ').trim() ?? ''
+}
+
+test('renders the table without crashing', async ({ page }) => {
+  const errors = await openExample(page)
+  const table = page.locator('table').first()
+
+  await expect(table).toBeVisible()
+  await expect(bodyRows(page)).toHaveCount(TOTAL_ROWS)
+  // Nothing is expanded, so no sub component rows exist yet.
+  await expect(subComponents(page)).toHaveCount(0)
+  await expect(rowExpander(page, 0)).toHaveText('👉')
+
+  expect(errors).toEqual([])
+})
+
+test('regenerates table data', async ({ page }) => {
+  const errors = await openExample(page)
+  const regenerateButton = page.getByRole('button', {
+    name: /^Regenerate Data$/i,
+  })
+
+  await expect(bodyRows(page).first()).toBeVisible()
+
+  const firstRowBefore = await getFirstBodyRowText(page)
+
+  await regenerateButton.click()
+
+  await expect.poll(() => getFirstBodyRowText(page)).not.toBe(firstRowBefore)
+  await expect(bodyRows(page)).toHaveCount(TOTAL_ROWS)
+
+  expect(errors).toEqual([])
+})
+
+test('expands a row to reveal its sub component', async ({ page }) => {
+  const errors = await openExample(page)
+
+  await rowExpander(page, 0).click()
+
+  await expect(rowExpander(page, 0)).toHaveText('👇')
+  await expect(subComponents(page)).toHaveCount(1)
+  // The sub component adds a full width row directly beneath its parent.
+  await expect(bodyRows(page)).toHaveCount(TOTAL_ROWS + 1)
+  await expect(bodyRows(page).nth(1).locator('td')).toHaveCount(1)
+
+  await rowExpander(page, 0).click()
+
+  await expect(rowExpander(page, 0)).toHaveText('👉')
+  await expect(subComponents(page)).toHaveCount(0)
+  await expect(bodyRows(page)).toHaveCount(TOTAL_ROWS)
+
+  expect(errors).toEqual([])
+})
+
+test('renders the expanded row own data in the sub component', async ({
+  page,
+}) => {
+  const errors = await openExample(page)
+
+  // Read a value straight out of the row before opening it, so the sub
+  // component can be checked against its own parent rather than any row.
+  const ageCell = await bodyRows(page)
+    .first()
+    .locator('td')
+    .nth(3)
+    .textContent()
+
+  await rowExpander(page, 0).click()
+  await expect(subComponents(page)).toHaveCount(1)
+
+  const original = await readSubComponentJson(page, 0)
+  expect(original['age']).toBe(Number(ageCell?.trim()))
+  expect(original).toHaveProperty('firstName')
+  expect(original).toHaveProperty('status')
+
+  expect(errors).toEqual([])
+})
+
+test('keeps several sub components open at once', async ({ page }) => {
+  const errors = await openExample(page)
+
+  await rowExpander(page, 0).click()
+  await expect(subComponents(page)).toHaveCount(1)
+
+  // Row 1 is now the first sub component row, so the next data row is row 2.
+  await rowExpander(page, 2).click()
+
+  await expect(subComponents(page)).toHaveCount(2)
+  await expect(bodyRows(page)).toHaveCount(TOTAL_ROWS + 2)
+  await expect(rowExpander(page, 0)).toHaveText('👇')
+  await expect(rowExpander(page, 2)).toHaveText('👇')
+
+  // Collapsing one leaves the other open.
+  await rowExpander(page, 0).click()
+
+  await expect(subComponents(page)).toHaveCount(1)
+  await expect(bodyRows(page)).toHaveCount(TOTAL_ROWS + 1)
+
+  expect(errors).toEqual([])
+})
diff --git a/examples/octane/sub-components/tsconfig.json b/examples/octane/sub-components/tsconfig.json
new file mode 100644
index 0000000000..a33b01356c
--- /dev/null
+++ b/examples/octane/sub-components/tsconfig.json
@@ -0,0 +1,22 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "useDefineForClassFields": true,
+    "lib": ["ES2022", "DOM", "DOM.Iterable"],
+    "module": "ESNext",
+    "skipLibCheck": true,
+    "moduleResolution": "Bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react-jsx",
+    "jsxImportSource": "octane",
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+    "types": ["node"]
+  },
+  "include": ["src", "vite.config.ts"]
+}
diff --git a/examples/octane/sub-components/vite.config.ts b/examples/octane/sub-components/vite.config.ts
new file mode 100644
index 0000000000..1317fe0821
--- /dev/null
+++ b/examples/octane/sub-components/vite.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from 'vite'
+import { octane } from 'octane/compiler/vite'
+
+export default defineConfig({
+  server: {
+    port: 7777,
+  },
+  plugins: [octane()],
+})
diff --git a/examples/preact/aggregation/src/main.tsx b/examples/preact/aggregation/src/main.tsx
index 725837e061..1ad9500ce6 100644
--- a/examples/preact/aggregation/src/main.tsx
+++ b/examples/preact/aggregation/src/main.tsx
@@ -131,7 +131,7 @@ function App() {
       debugTable: true,
       debugColumns: true,
     },
-    (state) => state,
+    (state) => state, // default selector
   )
 
   return (
diff --git a/examples/preact/aggregation/vite.config.ts b/examples/preact/aggregation/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/aggregation/vite.config.ts
+++ b/examples/preact/aggregation/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/basic-dynamic-columns/vite.config.ts b/examples/preact/basic-dynamic-columns/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/basic-dynamic-columns/vite.config.ts
+++ b/examples/preact/basic-dynamic-columns/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/basic-external-atoms/vite.config.ts b/examples/preact/basic-external-atoms/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/basic-external-atoms/vite.config.ts
+++ b/examples/preact/basic-external-atoms/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/basic-external-state/vite.config.ts b/examples/preact/basic-external-state/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/basic-external-state/vite.config.ts
+++ b/examples/preact/basic-external-state/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/basic-subscribe/vite.config.ts b/examples/preact/basic-subscribe/vite.config.ts
index 31bc8b4f16..3dd544a60e 100644
--- a/examples/preact/basic-subscribe/vite.config.ts
+++ b/examples/preact/basic-subscribe/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact({ babel: {} })],
 })
diff --git a/examples/preact/basic-use-app-table/vite.config.ts b/examples/preact/basic-use-app-table/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/basic-use-app-table/vite.config.ts
+++ b/examples/preact/basic-use-app-table/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/basic-use-table/vite.config.ts b/examples/preact/basic-use-table/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/basic-use-table/vite.config.ts
+++ b/examples/preact/basic-use-table/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/cell-selection/vite.config.ts b/examples/preact/cell-selection/vite.config.ts
index 31bc8b4f16..3dd544a60e 100644
--- a/examples/preact/cell-selection/vite.config.ts
+++ b/examples/preact/cell-selection/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact({ babel: {} })],
 })
diff --git a/examples/preact/column-ordering/vite.config.ts b/examples/preact/column-ordering/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/column-ordering/vite.config.ts
+++ b/examples/preact/column-ordering/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/column-pinning-split/vite.config.ts b/examples/preact/column-pinning-split/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/column-pinning-split/vite.config.ts
+++ b/examples/preact/column-pinning-split/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/column-pinning-sticky/vite.config.ts b/examples/preact/column-pinning-sticky/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/column-pinning-sticky/vite.config.ts
+++ b/examples/preact/column-pinning-sticky/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/column-pinning/vite.config.ts b/examples/preact/column-pinning/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/column-pinning/vite.config.ts
+++ b/examples/preact/column-pinning/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/column-resizing-performant/vite.config.ts b/examples/preact/column-resizing-performant/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/column-resizing-performant/vite.config.ts
+++ b/examples/preact/column-resizing-performant/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/column-resizing/vite.config.ts b/examples/preact/column-resizing/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/column-resizing/vite.config.ts
+++ b/examples/preact/column-resizing/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/column-sizing/vite.config.ts b/examples/preact/column-sizing/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/column-sizing/vite.config.ts
+++ b/examples/preact/column-sizing/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/column-visibility/vite.config.ts b/examples/preact/column-visibility/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/column-visibility/vite.config.ts
+++ b/examples/preact/column-visibility/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/composable-tables/vite.config.ts b/examples/preact/composable-tables/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/composable-tables/vite.config.ts
+++ b/examples/preact/composable-tables/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/custom-plugin/vite.config.ts b/examples/preact/custom-plugin/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/custom-plugin/vite.config.ts
+++ b/examples/preact/custom-plugin/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/expanding/vite.config.ts b/examples/preact/expanding/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/expanding/vite.config.ts
+++ b/examples/preact/expanding/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/filters-faceted-bucketed/src/main.tsx b/examples/preact/filters-faceted-bucketed/src/main.tsx
index 5da9dd62e5..59b0e65791 100644
--- a/examples/preact/filters-faceted-bucketed/src/main.tsx
+++ b/examples/preact/filters-faceted-bucketed/src/main.tsx
@@ -187,7 +187,7 @@ function App() {
       debugHeaders: true,
       debugColumns: false,
     },
-    (state) => state,
+    (state) => state, // default selector
   )
 
   return (
diff --git a/examples/preact/filters-faceted-bucketed/vite.config.ts b/examples/preact/filters-faceted-bucketed/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/filters-faceted-bucketed/vite.config.ts
+++ b/examples/preact/filters-faceted-bucketed/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/filters-faceted/vite.config.ts b/examples/preact/filters-faceted/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/filters-faceted/vite.config.ts
+++ b/examples/preact/filters-faceted/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/filters-fuzzy/vite.config.ts b/examples/preact/filters-fuzzy/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/filters-fuzzy/vite.config.ts
+++ b/examples/preact/filters-fuzzy/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/filters/vite.config.ts b/examples/preact/filters/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/filters/vite.config.ts
+++ b/examples/preact/filters/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/grouped-aggregation/vite.config.ts b/examples/preact/grouped-aggregation/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/grouped-aggregation/vite.config.ts
+++ b/examples/preact/grouped-aggregation/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/grouping/vite.config.ts b/examples/preact/grouping/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/grouping/vite.config.ts
+++ b/examples/preact/grouping/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/header-groups/vite.config.ts b/examples/preact/header-groups/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/header-groups/vite.config.ts
+++ b/examples/preact/header-groups/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/kitchen-sink/src/main.tsx b/examples/preact/kitchen-sink/src/main.tsx
index f95b7ee74e..90dcac8930 100644
--- a/examples/preact/kitchen-sink/src/main.tsx
+++ b/examples/preact/kitchen-sink/src/main.tsx
@@ -588,7 +588,7 @@ function App() {
       keepPinnedRows: true,
       debugTable: true,
     },
-    (state) => state,
+    (state) => state, // default selector
   )
 
   useTanStackTableDevtools(table)
diff --git a/examples/preact/kitchen-sink/vite.config.ts b/examples/preact/kitchen-sink/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/kitchen-sink/vite.config.ts
+++ b/examples/preact/kitchen-sink/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/pagination/vite.config.ts b/examples/preact/pagination/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/pagination/vite.config.ts
+++ b/examples/preact/pagination/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/row-pinning/vite.config.ts b/examples/preact/row-pinning/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/row-pinning/vite.config.ts
+++ b/examples/preact/row-pinning/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/row-selection/vite.config.ts b/examples/preact/row-selection/vite.config.ts
index 31bc8b4f16..3dd544a60e 100644
--- a/examples/preact/row-selection/vite.config.ts
+++ b/examples/preact/row-selection/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact({ babel: {} })],
 })
diff --git a/examples/preact/sorting/vite.config.ts b/examples/preact/sorting/vite.config.ts
index 40091af6b9..5a1cb23428 100644
--- a/examples/preact/sorting/vite.config.ts
+++ b/examples/preact/sorting/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     // Force the Babel transform path so dev mode avoids the preset's CJS
diff --git a/examples/preact/sub-components/vite.config.ts b/examples/preact/sub-components/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/sub-components/vite.config.ts
+++ b/examples/preact/sub-components/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/with-tanstack-form/src/main.tsx b/examples/preact/with-tanstack-form/src/main.tsx
index d660dea64c..d14bb3266f 100644
--- a/examples/preact/with-tanstack-form/src/main.tsx
+++ b/examples/preact/with-tanstack-form/src/main.tsx
@@ -141,7 +141,7 @@ function FullTableFormExample() {
       data,
       debugTable: true,
     },
-    (state) => state,
+    (state) => state, // default selector
   )
 
   useTanStackTableDevtools(table)
@@ -331,7 +331,7 @@ function RowSubmitFormExample() {
       data,
       debugTable: true,
     },
-    (state) => state,
+    (state) => state, // default selector
   )
 
   useTanStackTableDevtools(table)
diff --git a/examples/preact/with-tanstack-form/vite.config.ts b/examples/preact/with-tanstack-form/vite.config.ts
index 6e8ec63745..6bed4e042f 100644
--- a/examples/preact/with-tanstack-form/vite.config.ts
+++ b/examples/preact/with-tanstack-form/vite.config.ts
@@ -3,7 +3,7 @@ import preact from '@preact/preset-vite'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/preact/with-tanstack-query/vite.config.ts b/examples/preact/with-tanstack-query/vite.config.ts
index 6b46e37cc9..eb004c48a6 100644
--- a/examples/preact/with-tanstack-query/vite.config.ts
+++ b/examples/preact/with-tanstack-query/vite.config.ts
@@ -4,7 +4,7 @@ import preact from '@preact/preset-vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [preact()],
 })
diff --git a/examples/react/aggregation/vite.config.js b/examples/react/aggregation/vite.config.js
index bf221a8f6c..854e31524f 100644
--- a/examples/react/aggregation/vite.config.js
+++ b/examples/react/aggregation/vite.config.js
@@ -4,7 +4,7 @@ import babel from '@rolldown/plugin-babel'
 import rollupReplace from '@rollup/plugin-replace'
 
 export default defineConfig({
-  server: { port: 6565 },
+  server: { port: 7777 },
   plugins: [
     rollupReplace({
       preventAssignment: true,
diff --git a/examples/react/basic-dynamic-columns/vite.config.js b/examples/react/basic-dynamic-columns/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/basic-dynamic-columns/vite.config.js
+++ b/examples/react/basic-dynamic-columns/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/basic-external-atoms/vite.config.js b/examples/react/basic-external-atoms/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/basic-external-atoms/vite.config.js
+++ b/examples/react/basic-external-atoms/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/basic-external-state/vite.config.js b/examples/react/basic-external-state/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/basic-external-state/vite.config.js
+++ b/examples/react/basic-external-state/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/basic-subscribe/vite.config.js b/examples/react/basic-subscribe/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/basic-subscribe/vite.config.js
+++ b/examples/react/basic-subscribe/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/basic-use-app-table/vite.config.js b/examples/react/basic-use-app-table/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/basic-use-app-table/vite.config.js
+++ b/examples/react/basic-use-app-table/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/basic-use-legacy-table/vite.config.js b/examples/react/basic-use-legacy-table/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/basic-use-legacy-table/vite.config.js
+++ b/examples/react/basic-use-legacy-table/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/basic-use-table/vite.config.js b/examples/react/basic-use-table/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/basic-use-table/vite.config.js
+++ b/examples/react/basic-use-table/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/cell-selection/vite.config.js b/examples/react/cell-selection/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/cell-selection/vite.config.js
+++ b/examples/react/cell-selection/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/column-dnd/vite.config.js b/examples/react/column-dnd/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/column-dnd/vite.config.js
+++ b/examples/react/column-dnd/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/column-ordering/vite.config.js b/examples/react/column-ordering/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/column-ordering/vite.config.js
+++ b/examples/react/column-ordering/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/column-pinning-split/vite.config.js b/examples/react/column-pinning-split/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/column-pinning-split/vite.config.js
+++ b/examples/react/column-pinning-split/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/column-pinning-sticky/vite.config.js b/examples/react/column-pinning-sticky/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/column-pinning-sticky/vite.config.js
+++ b/examples/react/column-pinning-sticky/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/column-pinning/vite.config.js b/examples/react/column-pinning/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/column-pinning/vite.config.js
+++ b/examples/react/column-pinning/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/column-resizing-performant/vite.config.js b/examples/react/column-resizing-performant/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/column-resizing-performant/vite.config.js
+++ b/examples/react/column-resizing-performant/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/column-resizing/vite.config.js b/examples/react/column-resizing/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/column-resizing/vite.config.js
+++ b/examples/react/column-resizing/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/column-sizing/vite.config.js b/examples/react/column-sizing/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/column-sizing/vite.config.js
+++ b/examples/react/column-sizing/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/column-visibility/vite.config.js b/examples/react/column-visibility/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/column-visibility/vite.config.js
+++ b/examples/react/column-visibility/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/composable-tables/vite.config.js b/examples/react/composable-tables/vite.config.js
index 09d75b4c4b..699b3f4ceb 100644
--- a/examples/react/composable-tables/vite.config.js
+++ b/examples/react/composable-tables/vite.config.js
@@ -5,7 +5,7 @@ import babel from '@rolldown/plugin-babel'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     react(),
diff --git a/examples/react/custom-plugin/vite.config.js b/examples/react/custom-plugin/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/custom-plugin/vite.config.js
+++ b/examples/react/custom-plugin/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/expanding/vite.config.js b/examples/react/expanding/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/expanding/vite.config.js
+++ b/examples/react/expanding/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/filters-faceted-bucketed/src/main.tsx b/examples/react/filters-faceted-bucketed/src/main.tsx
index bbb4befbb0..2cf4505d4a 100644
--- a/examples/react/filters-faceted-bucketed/src/main.tsx
+++ b/examples/react/filters-faceted-bucketed/src/main.tsx
@@ -186,7 +186,7 @@ function App() {
       debugHeaders: true,
       debugColumns: false,
     },
-    (state) => state,
+    (state) => state, // default selector
   )
 
   return (
diff --git a/examples/react/filters-faceted-bucketed/vite.config.js b/examples/react/filters-faceted-bucketed/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/filters-faceted-bucketed/vite.config.js
+++ b/examples/react/filters-faceted-bucketed/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/filters-faceted/vite.config.js b/examples/react/filters-faceted/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/filters-faceted/vite.config.js
+++ b/examples/react/filters-faceted/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/filters-fuzzy/vite.config.js b/examples/react/filters-fuzzy/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/filters-fuzzy/vite.config.js
+++ b/examples/react/filters-fuzzy/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/filters/vite.config.js b/examples/react/filters/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/filters/vite.config.js
+++ b/examples/react/filters/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/grouped-aggregation/vite.config.js b/examples/react/grouped-aggregation/vite.config.js
index bf221a8f6c..854e31524f 100644
--- a/examples/react/grouped-aggregation/vite.config.js
+++ b/examples/react/grouped-aggregation/vite.config.js
@@ -4,7 +4,7 @@ import babel from '@rolldown/plugin-babel'
 import rollupReplace from '@rollup/plugin-replace'
 
 export default defineConfig({
-  server: { port: 6565 },
+  server: { port: 7777 },
   plugins: [
     rollupReplace({
       preventAssignment: true,
diff --git a/examples/react/grouping/vite.config.js b/examples/react/grouping/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/grouping/vite.config.js
+++ b/examples/react/grouping/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/header-groups/vite.config.js b/examples/react/header-groups/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/header-groups/vite.config.js
+++ b/examples/react/header-groups/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/kitchen-sink-chakra-ui/src/main.tsx b/examples/react/kitchen-sink-chakra-ui/src/main.tsx
index dbddb189ab..c4b2ce0b82 100644
--- a/examples/react/kitchen-sink-chakra-ui/src/main.tsx
+++ b/examples/react/kitchen-sink-chakra-ui/src/main.tsx
@@ -212,7 +212,7 @@ function App() {
         columnOrder: columns.map((c) => c.id ?? ''),
       },
     },
-    (state) => state,
+    (state) => state, // default selector
   )
 
   useTanStackTableDevtools(table)
diff --git a/examples/react/kitchen-sink-chakra-ui/vite.config.js b/examples/react/kitchen-sink-chakra-ui/vite.config.js
index bbbede86a4..113d017014 100644
--- a/examples/react/kitchen-sink-chakra-ui/vite.config.js
+++ b/examples/react/kitchen-sink-chakra-ui/vite.config.js
@@ -7,7 +7,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/kitchen-sink-hero-ui/src/main.tsx b/examples/react/kitchen-sink-hero-ui/src/main.tsx
index 10bb3335eb..2df61997dd 100644
--- a/examples/react/kitchen-sink-hero-ui/src/main.tsx
+++ b/examples/react/kitchen-sink-hero-ui/src/main.tsx
@@ -145,7 +145,7 @@ function App() {
         columnOrder: columns.map((c) => c.id ?? ''),
       },
     },
-    (state) => state,
+    (state) => state, // default selector
   )
 
   useTanStackTableDevtools(table)
diff --git a/examples/react/kitchen-sink-hero-ui/vite.config.js b/examples/react/kitchen-sink-hero-ui/vite.config.js
index a1446a39eb..ead8f0ceb3 100644
--- a/examples/react/kitchen-sink-hero-ui/vite.config.js
+++ b/examples/react/kitchen-sink-hero-ui/vite.config.js
@@ -8,7 +8,7 @@ import tailwindcss from '@tailwindcss/vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/kitchen-sink-mantine/src/main.tsx b/examples/react/kitchen-sink-mantine/src/main.tsx
index 96c457a425..c6ccee3993 100644
--- a/examples/react/kitchen-sink-mantine/src/main.tsx
+++ b/examples/react/kitchen-sink-mantine/src/main.tsx
@@ -194,7 +194,7 @@ function App() {
         columnOrder: columns.map((c) => c.id ?? ''),
       },
     },
-    (state) => state,
+    (state) => state, // default selector
   )
 
   useTanStackTableDevtools(table)
diff --git a/examples/react/kitchen-sink-mantine/vite.config.js b/examples/react/kitchen-sink-mantine/vite.config.js
index bbbede86a4..113d017014 100644
--- a/examples/react/kitchen-sink-mantine/vite.config.js
+++ b/examples/react/kitchen-sink-mantine/vite.config.js
@@ -7,7 +7,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/kitchen-sink-material-ui/src/main.tsx b/examples/react/kitchen-sink-material-ui/src/main.tsx
index e408bfbf08..4aea80e96b 100644
--- a/examples/react/kitchen-sink-material-ui/src/main.tsx
+++ b/examples/react/kitchen-sink-material-ui/src/main.tsx
@@ -110,7 +110,7 @@ function App({
         columnOrder: columns.map((c) => c.id ?? ''),
       },
     },
-    (state) => state,
+    (state) => state, // default selector
   )
 
   useTanStackTableDevtools(table)
diff --git a/examples/react/kitchen-sink-material-ui/vite.config.js b/examples/react/kitchen-sink-material-ui/vite.config.js
index bbbede86a4..113d017014 100644
--- a/examples/react/kitchen-sink-material-ui/vite.config.js
+++ b/examples/react/kitchen-sink-material-ui/vite.config.js
@@ -7,7 +7,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/kitchen-sink-react-aria/src/main.tsx b/examples/react/kitchen-sink-react-aria/src/main.tsx
index d9e9a75c3f..f36110312a 100644
--- a/examples/react/kitchen-sink-react-aria/src/main.tsx
+++ b/examples/react/kitchen-sink-react-aria/src/main.tsx
@@ -153,7 +153,7 @@ function App() {
         columnOrder: columns.map((c) => c.id ?? ''),
       },
     },
-    (state) => state, // default state selector
+    (state) => state, // default selector
   )
 
   useTanStackTableDevtools(table)
diff --git a/examples/react/kitchen-sink-react-aria/vite.config.js b/examples/react/kitchen-sink-react-aria/vite.config.js
index a1446a39eb..ead8f0ceb3 100644
--- a/examples/react/kitchen-sink-react-aria/vite.config.js
+++ b/examples/react/kitchen-sink-react-aria/vite.config.js
@@ -8,7 +8,7 @@ import tailwindcss from '@tailwindcss/vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/kitchen-sink-shadcn-base/src/main.tsx b/examples/react/kitchen-sink-shadcn-base/src/main.tsx
index 4e77fd8115..8aa68c3474 100644
--- a/examples/react/kitchen-sink-shadcn-base/src/main.tsx
+++ b/examples/react/kitchen-sink-shadcn-base/src/main.tsx
@@ -88,7 +88,7 @@ function App() {
         columnOrder: columns.map((c) => c.id ?? ''),
       },
     },
-    (state) => state, // default state selector
+    (state) => state, // default selector
   )
 
   useTanStackTableDevtools(table)
diff --git a/examples/react/kitchen-sink-shadcn-base/vite.config.js b/examples/react/kitchen-sink-shadcn-base/vite.config.js
index a1446a39eb..ead8f0ceb3 100644
--- a/examples/react/kitchen-sink-shadcn-base/vite.config.js
+++ b/examples/react/kitchen-sink-shadcn-base/vite.config.js
@@ -8,7 +8,7 @@ import tailwindcss from '@tailwindcss/vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/kitchen-sink-shadcn-radix/src/main.tsx b/examples/react/kitchen-sink-shadcn-radix/src/main.tsx
index 5fe06f71c3..0c6fae490b 100644
--- a/examples/react/kitchen-sink-shadcn-radix/src/main.tsx
+++ b/examples/react/kitchen-sink-shadcn-radix/src/main.tsx
@@ -88,7 +88,7 @@ function App() {
         columnOrder: columns.map((c) => c.id ?? ''),
       },
     },
-    (state) => state, // default state selector
+    (state) => state, // default selector
   )
 
   useTanStackTableDevtools(table)
diff --git a/examples/react/kitchen-sink-shadcn-radix/vite.config.js b/examples/react/kitchen-sink-shadcn-radix/vite.config.js
index a1446a39eb..ead8f0ceb3 100644
--- a/examples/react/kitchen-sink-shadcn-radix/vite.config.js
+++ b/examples/react/kitchen-sink-shadcn-radix/vite.config.js
@@ -8,7 +8,7 @@ import tailwindcss from '@tailwindcss/vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/kitchen-sink/vite.config.ts b/examples/react/kitchen-sink/vite.config.ts
index 8dcbbf43e8..3a378066ad 100644
--- a/examples/react/kitchen-sink/vite.config.ts
+++ b/examples/react/kitchen-sink/vite.config.ts
@@ -9,7 +9,7 @@ export default defineConfig({
     __DEV__: JSON.stringify(true),
   },
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     tanstackStart(),
diff --git a/examples/react/lib-chakra-ui/vite.config.js b/examples/react/lib-chakra-ui/vite.config.js
index a519d275fa..082a897574 100644
--- a/examples/react/lib-chakra-ui/vite.config.js
+++ b/examples/react/lib-chakra-ui/vite.config.js
@@ -7,7 +7,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/lib-hero-ui/vite.config.js b/examples/react/lib-hero-ui/vite.config.js
index 8f9b06f24a..9fbaaa7f6a 100644
--- a/examples/react/lib-hero-ui/vite.config.js
+++ b/examples/react/lib-hero-ui/vite.config.js
@@ -8,7 +8,7 @@ import tailwindcss from '@tailwindcss/vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/lib-mantine/vite.config.js b/examples/react/lib-mantine/vite.config.js
index a519d275fa..082a897574 100644
--- a/examples/react/lib-mantine/vite.config.js
+++ b/examples/react/lib-mantine/vite.config.js
@@ -7,7 +7,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/lib-material-ui/vite.config.js b/examples/react/lib-material-ui/vite.config.js
index a519d275fa..082a897574 100644
--- a/examples/react/lib-material-ui/vite.config.js
+++ b/examples/react/lib-material-ui/vite.config.js
@@ -7,7 +7,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/lib-react-aria/vite.config.js b/examples/react/lib-react-aria/vite.config.js
index 8f9b06f24a..9fbaaa7f6a 100644
--- a/examples/react/lib-react-aria/vite.config.js
+++ b/examples/react/lib-react-aria/vite.config.js
@@ -8,7 +8,7 @@ import tailwindcss from '@tailwindcss/vite'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/lib-shadcn-base/vite.config.js b/examples/react/lib-shadcn-base/vite.config.js
index a8add03b5a..7d05ac8129 100644
--- a/examples/react/lib-shadcn-base/vite.config.js
+++ b/examples/react/lib-shadcn-base/vite.config.js
@@ -8,7 +8,7 @@ import * as path from 'path'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/lib-shadcn-radix/vite.config.js b/examples/react/lib-shadcn-radix/vite.config.js
index a8add03b5a..7d05ac8129 100644
--- a/examples/react/lib-shadcn-radix/vite.config.js
+++ b/examples/react/lib-shadcn-radix/vite.config.js
@@ -8,7 +8,7 @@ import * as path from 'path'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/mantine-react-table/vite.config.js b/examples/react/mantine-react-table/vite.config.js
index bbbede86a4..113d017014 100644
--- a/examples/react/mantine-react-table/vite.config.js
+++ b/examples/react/mantine-react-table/vite.config.js
@@ -7,7 +7,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/material-react-table/vite.config.js b/examples/react/material-react-table/vite.config.js
index bbbede86a4..113d017014 100644
--- a/examples/react/material-react-table/vite.config.js
+++ b/examples/react/material-react-table/vite.config.js
@@ -7,7 +7,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/pagination/vite.config.js b/examples/react/pagination/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/pagination/vite.config.js
+++ b/examples/react/pagination/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/row-dnd/vite.config.js b/examples/react/row-dnd/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/row-dnd/vite.config.js
+++ b/examples/react/row-dnd/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/row-pinning/vite.config.js b/examples/react/row-pinning/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/row-pinning/vite.config.js
+++ b/examples/react/row-pinning/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/row-selection/vite.config.js b/examples/react/row-selection/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/row-selection/vite.config.js
+++ b/examples/react/row-selection/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/sorting/vite.config.js b/examples/react/sorting/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/sorting/vite.config.js
+++ b/examples/react/sorting/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/spreadsheet/vite.config.js b/examples/react/spreadsheet/vite.config.js
index 7476ecdb52..d24b86cc10 100644
--- a/examples/react/spreadsheet/vite.config.js
+++ b/examples/react/spreadsheet/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/sub-components/vite.config.js b/examples/react/sub-components/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/sub-components/vite.config.js
+++ b/examples/react/sub-components/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/virtualized-columns-experimental/vite.config.js b/examples/react/virtualized-columns-experimental/vite.config.js
index 08f616624a..8095778483 100644
--- a/examples/react/virtualized-columns-experimental/vite.config.js
+++ b/examples/react/virtualized-columns-experimental/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/virtualized-columns/vite.config.js b/examples/react/virtualized-columns/vite.config.js
index 02d0b1fff9..fd2203b0ab 100644
--- a/examples/react/virtualized-columns/vite.config.js
+++ b/examples/react/virtualized-columns/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/virtualized-infinite-scrolling/vite.config.js b/examples/react/virtualized-infinite-scrolling/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/virtualized-infinite-scrolling/vite.config.js
+++ b/examples/react/virtualized-infinite-scrolling/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/virtualized-rows-experimental/vite.config.js b/examples/react/virtualized-rows-experimental/vite.config.js
index 02d0b1fff9..fd2203b0ab 100644
--- a/examples/react/virtualized-rows-experimental/vite.config.js
+++ b/examples/react/virtualized-rows-experimental/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/virtualized-rows/vite.config.js b/examples/react/virtualized-rows/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/virtualized-rows/vite.config.js
+++ b/examples/react/virtualized-rows/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/web-worker-row-models/vite.config.ts b/examples/react/web-worker-row-models/vite.config.ts
index 8dcbbf43e8..3a378066ad 100644
--- a/examples/react/web-worker-row-models/vite.config.ts
+++ b/examples/react/web-worker-row-models/vite.config.ts
@@ -9,7 +9,7 @@ export default defineConfig({
     __DEV__: JSON.stringify(true),
   },
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     tanstackStart(),
diff --git a/examples/react/with-tanstack-form/src/main.tsx b/examples/react/with-tanstack-form/src/main.tsx
index 6353609e9a..9e040ab6e1 100644
--- a/examples/react/with-tanstack-form/src/main.tsx
+++ b/examples/react/with-tanstack-form/src/main.tsx
@@ -141,7 +141,7 @@ function FullTableFormExample() {
       data,
       debugTable: true,
     },
-    (state) => state,
+    (state) => state, // default selector
   )
 
   useTanStackTableDevtools(table)
@@ -331,7 +331,7 @@ function RowSubmitFormExample() {
       data,
       debugTable: true,
     },
-    (state) => state,
+    (state) => state, // default selector
   )
 
   useTanStackTableDevtools(table)
diff --git a/examples/react/with-tanstack-form/vite.config.js b/examples/react/with-tanstack-form/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/with-tanstack-form/vite.config.js
+++ b/examples/react/with-tanstack-form/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/with-tanstack-query/vite.config.js b/examples/react/with-tanstack-query/vite.config.js
index e6b8eb7308..b869ded487 100644
--- a/examples/react/with-tanstack-query/vite.config.js
+++ b/examples/react/with-tanstack-query/vite.config.js
@@ -6,7 +6,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/react/with-tanstack-router/vite.config.js b/examples/react/with-tanstack-router/vite.config.js
index 3930375129..1454c6a030 100644
--- a/examples/react/with-tanstack-router/vite.config.js
+++ b/examples/react/with-tanstack-router/vite.config.js
@@ -7,7 +7,7 @@ import { TanStackRouterVite } from '@tanstack/router-vite-plugin'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/solid/aggregation/vite.config.ts b/examples/solid/aggregation/vite.config.ts
index ae07c754c1..71e6d80a4c 100644
--- a/examples/solid/aggregation/vite.config.ts
+++ b/examples/solid/aggregation/vite.config.ts
@@ -2,7 +2,7 @@ import { defineConfig } from 'vite'
 import solidPlugin from 'vite-plugin-solid'
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: { target: 'esnext' },
diff --git a/examples/solid/basic-app-table/vite.config.ts b/examples/solid/basic-app-table/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/basic-app-table/vite.config.ts
+++ b/examples/solid/basic-app-table/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/basic-dynamic-columns/vite.config.ts b/examples/solid/basic-dynamic-columns/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/basic-dynamic-columns/vite.config.ts
+++ b/examples/solid/basic-dynamic-columns/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/basic-external-atoms/vite.config.ts b/examples/solid/basic-external-atoms/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/basic-external-atoms/vite.config.ts
+++ b/examples/solid/basic-external-atoms/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/basic-external-state/vite.config.ts b/examples/solid/basic-external-state/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/basic-external-state/vite.config.ts
+++ b/examples/solid/basic-external-state/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/basic-use-table/vite.config.ts b/examples/solid/basic-use-table/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/basic-use-table/vite.config.ts
+++ b/examples/solid/basic-use-table/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/cell-selection/vite.config.ts b/examples/solid/cell-selection/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/cell-selection/vite.config.ts
+++ b/examples/solid/cell-selection/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/column-ordering/vite.config.ts b/examples/solid/column-ordering/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/column-ordering/vite.config.ts
+++ b/examples/solid/column-ordering/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/column-pinning-split/vite.config.ts b/examples/solid/column-pinning-split/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/column-pinning-split/vite.config.ts
+++ b/examples/solid/column-pinning-split/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/column-pinning-sticky/vite.config.ts b/examples/solid/column-pinning-sticky/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/column-pinning-sticky/vite.config.ts
+++ b/examples/solid/column-pinning-sticky/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/column-pinning/vite.config.ts b/examples/solid/column-pinning/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/column-pinning/vite.config.ts
+++ b/examples/solid/column-pinning/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/column-resizing-performant/vite.config.ts b/examples/solid/column-resizing-performant/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/column-resizing-performant/vite.config.ts
+++ b/examples/solid/column-resizing-performant/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/column-resizing/vite.config.ts b/examples/solid/column-resizing/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/column-resizing/vite.config.ts
+++ b/examples/solid/column-resizing/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/column-sizing/vite.config.ts b/examples/solid/column-sizing/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/column-sizing/vite.config.ts
+++ b/examples/solid/column-sizing/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/column-visibility/vite.config.ts b/examples/solid/column-visibility/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/column-visibility/vite.config.ts
+++ b/examples/solid/column-visibility/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/composable-tables/vite.config.ts b/examples/solid/composable-tables/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/composable-tables/vite.config.ts
+++ b/examples/solid/composable-tables/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/expanding/vite.config.ts b/examples/solid/expanding/vite.config.ts
index ae07c754c1..71e6d80a4c 100644
--- a/examples/solid/expanding/vite.config.ts
+++ b/examples/solid/expanding/vite.config.ts
@@ -2,7 +2,7 @@ import { defineConfig } from 'vite'
 import solidPlugin from 'vite-plugin-solid'
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: { target: 'esnext' },
diff --git a/examples/solid/filters-faceted-bucketed/vite.config.ts b/examples/solid/filters-faceted-bucketed/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/filters-faceted-bucketed/vite.config.ts
+++ b/examples/solid/filters-faceted-bucketed/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/filters-faceted/vite.config.ts b/examples/solid/filters-faceted/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/filters-faceted/vite.config.ts
+++ b/examples/solid/filters-faceted/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/filters-fuzzy/vite.config.ts b/examples/solid/filters-fuzzy/vite.config.ts
index ae07c754c1..71e6d80a4c 100644
--- a/examples/solid/filters-fuzzy/vite.config.ts
+++ b/examples/solid/filters-fuzzy/vite.config.ts
@@ -2,7 +2,7 @@ import { defineConfig } from 'vite'
 import solidPlugin from 'vite-plugin-solid'
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: { target: 'esnext' },
diff --git a/examples/solid/filters/vite.config.ts b/examples/solid/filters/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/filters/vite.config.ts
+++ b/examples/solid/filters/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/grouped-aggregation/vite.config.ts b/examples/solid/grouped-aggregation/vite.config.ts
index 3d89efe64b..04b8933bb4 100644
--- a/examples/solid/grouped-aggregation/vite.config.ts
+++ b/examples/solid/grouped-aggregation/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: { target: 'esnext' },
diff --git a/examples/solid/grouping/vite.config.ts b/examples/solid/grouping/vite.config.ts
index 3d89efe64b..04b8933bb4 100644
--- a/examples/solid/grouping/vite.config.ts
+++ b/examples/solid/grouping/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: { target: 'esnext' },
diff --git a/examples/solid/header-groups/vite.config.ts b/examples/solid/header-groups/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/header-groups/vite.config.ts
+++ b/examples/solid/header-groups/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/kitchen-sink/vite.config.ts b/examples/solid/kitchen-sink/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/kitchen-sink/vite.config.ts
+++ b/examples/solid/kitchen-sink/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/pagination/vite.config.ts b/examples/solid/pagination/vite.config.ts
index ae07c754c1..71e6d80a4c 100644
--- a/examples/solid/pagination/vite.config.ts
+++ b/examples/solid/pagination/vite.config.ts
@@ -2,7 +2,7 @@ import { defineConfig } from 'vite'
 import solidPlugin from 'vite-plugin-solid'
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: { target: 'esnext' },
diff --git a/examples/solid/row-pinning/vite.config.ts b/examples/solid/row-pinning/vite.config.ts
index 3d89efe64b..04b8933bb4 100644
--- a/examples/solid/row-pinning/vite.config.ts
+++ b/examples/solid/row-pinning/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: { target: 'esnext' },
diff --git a/examples/solid/row-selection/vite.config.ts b/examples/solid/row-selection/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/row-selection/vite.config.ts
+++ b/examples/solid/row-selection/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/sorting/vite.config.ts b/examples/solid/sorting/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/sorting/vite.config.ts
+++ b/examples/solid/sorting/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/sub-components/vite.config.ts b/examples/solid/sub-components/vite.config.ts
index ae07c754c1..71e6d80a4c 100644
--- a/examples/solid/sub-components/vite.config.ts
+++ b/examples/solid/sub-components/vite.config.ts
@@ -2,7 +2,7 @@ import { defineConfig } from 'vite'
 import solidPlugin from 'vite-plugin-solid'
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: { target: 'esnext' },
diff --git a/examples/solid/virtualized-columns/vite.config.ts b/examples/solid/virtualized-columns/vite.config.ts
index 3d89efe64b..04b8933bb4 100644
--- a/examples/solid/virtualized-columns/vite.config.ts
+++ b/examples/solid/virtualized-columns/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: { target: 'esnext' },
diff --git a/examples/solid/virtualized-infinite-scrolling/vite.config.ts b/examples/solid/virtualized-infinite-scrolling/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/virtualized-infinite-scrolling/vite.config.ts
+++ b/examples/solid/virtualized-infinite-scrolling/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/virtualized-rows/vite.config.ts b/examples/solid/virtualized-rows/vite.config.ts
index 3d89efe64b..04b8933bb4 100644
--- a/examples/solid/virtualized-rows/vite.config.ts
+++ b/examples/solid/virtualized-rows/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: { target: 'esnext' },
diff --git a/examples/solid/with-tanstack-form/vite.config.ts b/examples/solid/with-tanstack-form/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/with-tanstack-form/vite.config.ts
+++ b/examples/solid/with-tanstack-form/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/with-tanstack-query/vite.config.ts b/examples/solid/with-tanstack-query/vite.config.ts
index 5c064be0ce..9463045637 100644
--- a/examples/solid/with-tanstack-query/vite.config.ts
+++ b/examples/solid/with-tanstack-query/vite.config.ts
@@ -3,7 +3,7 @@ import solidPlugin from 'vite-plugin-solid'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin()],
   build: {
diff --git a/examples/solid/with-tanstack-router/vite.config.ts b/examples/solid/with-tanstack-router/vite.config.ts
index 4a73ca742a..8ff132815f 100644
--- a/examples/solid/with-tanstack-router/vite.config.ts
+++ b/examples/solid/with-tanstack-router/vite.config.ts
@@ -4,7 +4,7 @@ import { TanStackRouterVite } from '@tanstack/router-vite-plugin'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [solidPlugin(), TanStackRouterVite({ target: 'solid' })],
   build: {
diff --git a/examples/svelte/aggregation/vite.config.js b/examples/svelte/aggregation/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/aggregation/vite.config.js
+++ b/examples/svelte/aggregation/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/basic-app-table/vite.config.js b/examples/svelte/basic-app-table/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/basic-app-table/vite.config.js
+++ b/examples/svelte/basic-app-table/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/basic-create-table/vite.config.js b/examples/svelte/basic-create-table/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/basic-create-table/vite.config.js
+++ b/examples/svelte/basic-create-table/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/basic-dynamic-columns/vite.config.js b/examples/svelte/basic-dynamic-columns/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/basic-dynamic-columns/vite.config.js
+++ b/examples/svelte/basic-dynamic-columns/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/basic-external-atoms/vite.config.js b/examples/svelte/basic-external-atoms/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/basic-external-atoms/vite.config.js
+++ b/examples/svelte/basic-external-atoms/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/basic-external-state/vite.config.js b/examples/svelte/basic-external-state/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/basic-external-state/vite.config.js
+++ b/examples/svelte/basic-external-state/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/basic-snippets/vite.config.js b/examples/svelte/basic-snippets/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/basic-snippets/vite.config.js
+++ b/examples/svelte/basic-snippets/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/cell-selection/vite.config.js b/examples/svelte/cell-selection/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/cell-selection/vite.config.js
+++ b/examples/svelte/cell-selection/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/column-ordering/vite.config.js b/examples/svelte/column-ordering/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/column-ordering/vite.config.js
+++ b/examples/svelte/column-ordering/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/column-pinning-split/vite.config.js b/examples/svelte/column-pinning-split/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/column-pinning-split/vite.config.js
+++ b/examples/svelte/column-pinning-split/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/column-pinning-sticky/vite.config.js b/examples/svelte/column-pinning-sticky/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/column-pinning-sticky/vite.config.js
+++ b/examples/svelte/column-pinning-sticky/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/column-pinning/vite.config.js b/examples/svelte/column-pinning/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/column-pinning/vite.config.js
+++ b/examples/svelte/column-pinning/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/column-resizing-performant/vite.config.js b/examples/svelte/column-resizing-performant/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/column-resizing-performant/vite.config.js
+++ b/examples/svelte/column-resizing-performant/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/column-resizing/vite.config.js b/examples/svelte/column-resizing/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/column-resizing/vite.config.js
+++ b/examples/svelte/column-resizing/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/column-sizing/vite.config.js b/examples/svelte/column-sizing/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/column-sizing/vite.config.js
+++ b/examples/svelte/column-sizing/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/column-visibility/vite.config.js b/examples/svelte/column-visibility/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/column-visibility/vite.config.js
+++ b/examples/svelte/column-visibility/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/composable-tables/vite.config.js b/examples/svelte/composable-tables/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/composable-tables/vite.config.js
+++ b/examples/svelte/composable-tables/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/expanding/vite.config.js b/examples/svelte/expanding/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/expanding/vite.config.js
+++ b/examples/svelte/expanding/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/filtering/vite.config.js b/examples/svelte/filtering/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/filtering/vite.config.js
+++ b/examples/svelte/filtering/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/filters-faceted-bucketed/vite.config.js b/examples/svelte/filters-faceted-bucketed/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/filters-faceted-bucketed/vite.config.js
+++ b/examples/svelte/filters-faceted-bucketed/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/filters-faceted/vite.config.js b/examples/svelte/filters-faceted/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/filters-faceted/vite.config.js
+++ b/examples/svelte/filters-faceted/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/filters-fuzzy/vite.config.js b/examples/svelte/filters-fuzzy/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/filters-fuzzy/vite.config.js
+++ b/examples/svelte/filters-fuzzy/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/grouped-aggregation/vite.config.js b/examples/svelte/grouped-aggregation/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/grouped-aggregation/vite.config.js
+++ b/examples/svelte/grouped-aggregation/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/grouping/vite.config.js b/examples/svelte/grouping/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/grouping/vite.config.js
+++ b/examples/svelte/grouping/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/header-groups/vite.config.js b/examples/svelte/header-groups/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/header-groups/vite.config.js
+++ b/examples/svelte/header-groups/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/kitchen-sink/vite.config.js b/examples/svelte/kitchen-sink/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/kitchen-sink/vite.config.js
+++ b/examples/svelte/kitchen-sink/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/pagination/vite.config.js b/examples/svelte/pagination/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/pagination/vite.config.js
+++ b/examples/svelte/pagination/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/row-pinning/vite.config.js b/examples/svelte/row-pinning/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/row-pinning/vite.config.js
+++ b/examples/svelte/row-pinning/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/row-selection/vite.config.js b/examples/svelte/row-selection/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/row-selection/vite.config.js
+++ b/examples/svelte/row-selection/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/sorting/vite.config.js b/examples/svelte/sorting/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/sorting/vite.config.js
+++ b/examples/svelte/sorting/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/sub-components/vite.config.js b/examples/svelte/sub-components/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/sub-components/vite.config.js
+++ b/examples/svelte/sub-components/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/virtualized-columns/vite.config.js b/examples/svelte/virtualized-columns/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/virtualized-columns/vite.config.js
+++ b/examples/svelte/virtualized-columns/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/virtualized-infinite-scrolling/src/App.svelte b/examples/svelte/virtualized-infinite-scrolling/src/App.svelte
index e2bb17d626..eec1effe9e 100644
--- a/examples/svelte/virtualized-infinite-scrolling/src/App.svelte
+++ b/examples/svelte/virtualized-infinite-scrolling/src/App.svelte
@@ -95,7 +95,7 @@
   const totalFetched = $derived(flatData.length)
 
   const fetchMoreOnBottomReached = (
-    containerRefElement?: HTMLDivElement | null,
+    containerRefElement: HTMLDivElement | null | undefined,
   ) => {
     if (containerRefElement) {
       const { scrollHeight, scrollTop, clientHeight } = containerRefElement
diff --git a/examples/svelte/virtualized-infinite-scrolling/vite.config.js b/examples/svelte/virtualized-infinite-scrolling/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/virtualized-infinite-scrolling/vite.config.js
+++ b/examples/svelte/virtualized-infinite-scrolling/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/virtualized-rows/vite.config.js b/examples/svelte/virtualized-rows/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/virtualized-rows/vite.config.js
+++ b/examples/svelte/virtualized-rows/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/with-tanstack-form/vite.config.js b/examples/svelte/with-tanstack-form/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/with-tanstack-form/vite.config.js
+++ b/examples/svelte/with-tanstack-form/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/svelte/with-tanstack-query/vite.config.js b/examples/svelte/with-tanstack-query/vite.config.js
index 3c3718bc18..36f2cda387 100644
--- a/examples/svelte/with-tanstack-query/vite.config.js
+++ b/examples/svelte/with-tanstack-query/vite.config.js
@@ -5,7 +5,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/vanilla/aggregation/vite.config.js b/examples/vanilla/aggregation/vite.config.js
index 9b5122fda5..7a74450ca4 100644
--- a/examples/vanilla/aggregation/vite.config.js
+++ b/examples/vanilla/aggregation/vite.config.js
@@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/vanilla/basic/vite.config.js b/examples/vanilla/basic/vite.config.js
index 9b5122fda5..7a74450ca4 100644
--- a/examples/vanilla/basic/vite.config.js
+++ b/examples/vanilla/basic/vite.config.js
@@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/vanilla/pagination/vite.config.js b/examples/vanilla/pagination/vite.config.js
index 9b5122fda5..7a74450ca4 100644
--- a/examples/vanilla/pagination/vite.config.js
+++ b/examples/vanilla/pagination/vite.config.js
@@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/vanilla/sorting/vite.config.js b/examples/vanilla/sorting/vite.config.js
index 9b5122fda5..7a74450ca4 100644
--- a/examples/vanilla/sorting/vite.config.js
+++ b/examples/vanilla/sorting/vite.config.js
@@ -4,7 +4,7 @@ import rollupReplace from '@rollup/plugin-replace'
 // https://vitejs.dev/config/
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [
     rollupReplace({
diff --git a/examples/vue/aggregation/vite.config.ts b/examples/vue/aggregation/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/aggregation/vite.config.ts
+++ b/examples/vue/aggregation/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/basic-dynamic-columns/vite.config.ts b/examples/vue/basic-dynamic-columns/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/basic-dynamic-columns/vite.config.ts
+++ b/examples/vue/basic-dynamic-columns/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/basic-external-atoms/vite.config.ts b/examples/vue/basic-external-atoms/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/basic-external-atoms/vite.config.ts
+++ b/examples/vue/basic-external-atoms/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/basic-external-state/vite.config.ts b/examples/vue/basic-external-state/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/basic-external-state/vite.config.ts
+++ b/examples/vue/basic-external-state/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/basic-use-app-table/vite.config.ts b/examples/vue/basic-use-app-table/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/basic-use-app-table/vite.config.ts
+++ b/examples/vue/basic-use-app-table/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/basic-use-table/vite.config.ts b/examples/vue/basic-use-table/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/basic-use-table/vite.config.ts
+++ b/examples/vue/basic-use-table/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/cell-selection/vite.config.ts b/examples/vue/cell-selection/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/cell-selection/vite.config.ts
+++ b/examples/vue/cell-selection/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/column-ordering/vite.config.ts b/examples/vue/column-ordering/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/column-ordering/vite.config.ts
+++ b/examples/vue/column-ordering/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/column-pinning-split/vite.config.ts b/examples/vue/column-pinning-split/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/column-pinning-split/vite.config.ts
+++ b/examples/vue/column-pinning-split/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/column-pinning-sticky/vite.config.ts b/examples/vue/column-pinning-sticky/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/column-pinning-sticky/vite.config.ts
+++ b/examples/vue/column-pinning-sticky/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/column-pinning/vite.config.ts b/examples/vue/column-pinning/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/column-pinning/vite.config.ts
+++ b/examples/vue/column-pinning/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/column-resizing-performant/vite.config.ts b/examples/vue/column-resizing-performant/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/column-resizing-performant/vite.config.ts
+++ b/examples/vue/column-resizing-performant/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/column-resizing/vite.config.ts b/examples/vue/column-resizing/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/column-resizing/vite.config.ts
+++ b/examples/vue/column-resizing/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/column-sizing/vite.config.ts b/examples/vue/column-sizing/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/column-sizing/vite.config.ts
+++ b/examples/vue/column-sizing/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/column-visibility/vite.config.ts b/examples/vue/column-visibility/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/column-visibility/vite.config.ts
+++ b/examples/vue/column-visibility/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/composable-tables/vite.config.ts b/examples/vue/composable-tables/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/composable-tables/vite.config.ts
+++ b/examples/vue/composable-tables/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/expanding/vite.config.ts b/examples/vue/expanding/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/expanding/vite.config.ts
+++ b/examples/vue/expanding/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/filters-faceted-bucketed/vite.config.ts b/examples/vue/filters-faceted-bucketed/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/filters-faceted-bucketed/vite.config.ts
+++ b/examples/vue/filters-faceted-bucketed/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/filters-faceted/vite.config.ts b/examples/vue/filters-faceted/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/filters-faceted/vite.config.ts
+++ b/examples/vue/filters-faceted/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/filters-fuzzy/vite.config.ts b/examples/vue/filters-fuzzy/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/filters-fuzzy/vite.config.ts
+++ b/examples/vue/filters-fuzzy/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/filters/vite.config.ts b/examples/vue/filters/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/filters/vite.config.ts
+++ b/examples/vue/filters/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/grouped-aggregation/vite.config.ts b/examples/vue/grouped-aggregation/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/grouped-aggregation/vite.config.ts
+++ b/examples/vue/grouped-aggregation/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/grouping/vite.config.ts b/examples/vue/grouping/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/grouping/vite.config.ts
+++ b/examples/vue/grouping/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/header-groups/vite.config.ts b/examples/vue/header-groups/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/header-groups/vite.config.ts
+++ b/examples/vue/header-groups/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/kitchen-sink/vite.config.ts b/examples/vue/kitchen-sink/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/kitchen-sink/vite.config.ts
+++ b/examples/vue/kitchen-sink/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/pagination/vite.config.ts b/examples/vue/pagination/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/pagination/vite.config.ts
+++ b/examples/vue/pagination/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/row-pinning/vite.config.ts b/examples/vue/row-pinning/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/row-pinning/vite.config.ts
+++ b/examples/vue/row-pinning/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/row-selection/vite.config.ts b/examples/vue/row-selection/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/row-selection/vite.config.ts
+++ b/examples/vue/row-selection/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/sorting/vite.config.ts b/examples/vue/sorting/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/sorting/vite.config.ts
+++ b/examples/vue/sorting/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/sub-components/vite.config.ts b/examples/vue/sub-components/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/sub-components/vite.config.ts
+++ b/examples/vue/sub-components/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/virtualized-columns/vite.config.ts b/examples/vue/virtualized-columns/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/virtualized-columns/vite.config.ts
+++ b/examples/vue/virtualized-columns/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/virtualized-infinite-scrolling/vite.config.ts b/examples/vue/virtualized-infinite-scrolling/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/virtualized-infinite-scrolling/vite.config.ts
+++ b/examples/vue/virtualized-infinite-scrolling/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/virtualized-rows/vite.config.ts b/examples/vue/virtualized-rows/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/virtualized-rows/vite.config.ts
+++ b/examples/vue/virtualized-rows/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/with-tanstack-form/vite.config.ts b/examples/vue/with-tanstack-form/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/with-tanstack-form/vite.config.ts
+++ b/examples/vue/with-tanstack-form/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/examples/vue/with-tanstack-query/vite.config.ts b/examples/vue/with-tanstack-query/vite.config.ts
index 0651f43b44..df828c9ef4 100644
--- a/examples/vue/with-tanstack-query/vite.config.ts
+++ b/examples/vue/with-tanstack-query/vite.config.ts
@@ -4,7 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 
 export default defineConfig({
   server: {
-    port: 6565,
+    port: 7777,
   },
   plugins: [vue(), vueJsx()],
 })
diff --git a/knip.json b/knip.json
index c1de8bdf83..aab19bc8a2 100644
--- a/knip.json
+++ b/knip.json
@@ -36,6 +36,20 @@
         "experimental-worker-plugin/index.ts!"
       ]
     },
+    "packages/octane-table": {
+      "entry": [
+        "src/index.ts!",
+        "src/Subscribe.tsrx!",
+        "src/createTableHook.tsrx!",
+        "src/useTable.tsrx!",
+        "src/internal.ts!",
+        "src/reactivity.ts!",
+        "src/static-functions.ts!",
+        "src/flex-render.ts!",
+        "src/experimental-worker-plugin.ts!",
+        "tests/type-tests/public-surface.tsx!"
+      ]
+    },
     "packages/svelte-table": {
       "entry": [
         "src/index.ts!",
diff --git a/package.json b/package.json
index 39c4fb247b..7b54121ee7 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
     "clean:build": "find . -name 'dist' -type d -prune -exec rm -rf {} +",
     "clean:node_modules": "find . -name 'node_modules' -type d -prune -exec rm -rf {} +",
     "dev": "pnpm run watch",
-    "copy:readme": "cp README.md packages/table-core/README.md && cp README.md packages/table-devtools/README.md && cp README.md packages/react-table/README.md && cp README.md packages/react-table-devtools/README.md && cp README.md packages/preact-table/README.md && cp README.md packages/preact-table-devtools/README.md && cp README.md packages/angular-table/README.md && cp README.md packages/ember-table/README.md && cp README.md packages/solid-table/README.md && cp README.md packages/solid-table-devtools/README.md && cp README.md packages/vue-table/README.md && cp README.md packages/vue-table-devtools/README.md && cp README.md packages/lit-table/README.md && cp README.md packages/svelte-table/README.md && cp README.md packages/match-sorter-utils/README.md && cp README.md packages/alpine-table/README.md",
+    "copy:readme": "cp README.md packages/table-core/README.md && cp README.md packages/table-devtools/README.md && cp README.md packages/react-table/README.md && cp README.md packages/react-table-devtools/README.md && cp README.md packages/preact-table/README.md && cp README.md packages/preact-table-devtools/README.md && cp README.md packages/angular-table/README.md && cp README.md packages/ember-table/README.md && cp README.md packages/solid-table/README.md && cp README.md packages/solid-table-devtools/README.md && cp README.md packages/vue-table/README.md && cp README.md packages/vue-table-devtools/README.md && cp README.md packages/lit-table/README.md && cp README.md packages/svelte-table/README.md && cp README.md packages/match-sorter-utils/README.md && cp README.md packages/alpine-table/README.md && cp README.md packages/octane-table/README.md",
     "generate-docs": "node scripts/generateDocs.js && pnpm run copy:readme",
     "format": "prettier --experimental-cli --ignore-unknown '**/*' --write",
     "lint:fix:all": "pnpm run format && nx run-many --targets=lint --fix",
@@ -98,6 +98,7 @@
     "@tanstack/ember-table": "workspace:*",
     "@tanstack/lit-table": "workspace:*",
     "@tanstack/match-sorter-utils": "workspace:*",
+    "@tanstack/octane-table": "workspace:*",
     "@tanstack/preact-table": "workspace:*",
     "@tanstack/preact-table-devtools": "workspace:*",
     "@tanstack/react-table": "workspace:*",
diff --git a/packages/octane-table/README.md b/packages/octane-table/README.md
new file mode 100644
index 0000000000..38d5c7ded0
--- /dev/null
+++ b/packages/octane-table/README.md
@@ -0,0 +1,129 @@
+
+ TanStack Table +
+ +
+ +
+ + npm downloads + + + github stars + + + bundle size + +
+ +
+ + semantic-release + +Best of JS + Follow @TanStack +
+ +### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/) + + + +# TanStack Table + +> [!NOTE] +> You may know TanStack Table by the adapter names: +> +> - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) +> - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) +> - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) +> - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) +> - [Vue Table](https://tanstack.com/table/alpha/docs/framework/vue/vue-table) +> - [Alpine Table](https://tanstack.com/table/alpha/docs/framework/alpine/alpine-table) +> - [Ember Table](https://tanstack.com/table/alpha/docs/framework/ember/ember-table) + +A headless table library for building powerful datagrids with full control over markup, styles, and behavior. + +- Framework‑agnostic core with bindings for React, Vue & Solid +- 100% customizable — bring your own UI, components, and styles +- Sorting, filtering, grouping, aggregation & row selection +- Lightweight, virtualizable & server‑side friendly + +### Read the Docs → + +## Using an AI Coding Agent? + +TanStack Table ships [TanStack Intent](https://github.com/TanStack/intent) skills inside each adapter package. After installing the library, run: + +```sh +npx @tanstack/intent@latest install +``` + +to add skill-loading guidance for your agent (Claude Code, Cursor, Copilot, etc.). The same CLI also exposes `intent list` to browse available skills and `intent load ` to print one for inspection. Skills version with the library — your agent gets guidance that matches the version of `@tanstack/-table` you installed. Only available for v9 and above. + +## Get Involved + +- We welcome issues and pull requests! +- Participate in [GitHub discussions](https://github.com/TanStack/table/discussions) +- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ) +- See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions + +## Partners + +
+ + + + + +
+ + + + + CodeRabbit + + + + + + + + Cloudflare + + + + + + + + AG Grid + + +
+ +
+Table & you? +

+We're looking for TanStack Table Partners to join our mission! Partner with us to push the boundaries of TanStack Table and build amazing things together. +

+LET'S CHAT +
+ +## Explore the TanStack Ecosystem + +- TanStack Config – Tooling for JS/TS packages +- TanStack DB – Reactive sync client store +- TanStack DevTools – Unified devtools panel +- TanStack Form – Type‑safe form state +- TanStack Pacer – Debouncing, throttling, batching
+- TanStack Query – Async state & caching +- TanStack Ranger – Range & slider primitives +- TanStack Router – Type‑safe routing, caching & URL state +- TanStack Start – Full‑stack SSR & streaming +- TanStack Store – Reactive data store +- TanStack Virtual – Virtualized rendering + +… and more at TanStack.com » + + diff --git a/packages/octane-table/docs-entry.d.ts b/packages/octane-table/docs-entry.d.ts new file mode 100644 index 0000000000..29ba5bede5 --- /dev/null +++ b/packages/octane-table/docs-entry.d.ts @@ -0,0 +1,6 @@ +export * from './src/FlexRender' +export * from './src/Subscribe.tsrx' +export * from './src/createTableHook.tsrx' +export * from './src/createTableHookContexts' +export * from './src/types' +export * from './src/useTable.tsrx' diff --git a/packages/octane-table/package.json b/packages/octane-table/package.json new file mode 100644 index 0000000000..2590014b91 --- /dev/null +++ b/packages/octane-table/package.json @@ -0,0 +1,69 @@ +{ + "name": "@tanstack/octane-table", + "version": "9.0.0-beta.65", + "description": "Headless UI for building powerful tables & datagrids for Octane.", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/table.git", + "directory": "packages/octane-table" + }, + "homepage": "https://tanstack.com/table", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "octane", + "table", + "octane-table", + "datagrid", + "tanstack-intent" + ], + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.d.ts", + "exports": { + ".": { + "types": "./src/index.d.ts", + "default": "./src/index.ts" + }, + "./experimental-worker-plugin": "./src/experimental-worker-plugin.ts", + "./flex-render": "./src/flex-render.ts", + "./static-functions": "./src/static-functions.ts", + "./package.json": "./package.json" + }, + "sideEffects": false, + "engines": { + "node": ">=22" + }, + "files": [ + "src", + "README.md" + ], + "scripts": { + "clean": "rimraf ./build && rimraf ./dist", + "test:eslint": "eslint ./src --ext .ts", + "test:lib": "vitest --passWithNoTests && vitest --config vitest.ssr.config.ts --passWithNoTests", + "test:lib:client": "vitest --passWithNoTests", + "test:lib:ssr": "vitest --config vitest.ssr.config.ts --passWithNoTests", + "test:lib:dev": "pnpm test:lib:client --watch", + "test:types": "tsrx-tsc --noEmit", + "test:build": "publint --strict", + "build": "tsrx-tsc --noEmit" + }, + "dependencies": { + "@tanstack/octane-store": "^0.12.0", + "@tanstack/table-core": "workspace:*" + }, + "devDependencies": { + "@tsrx/typescript-plugin": "^0.3.118", + "octane": "0.1.21", + "typescript": "6.0.3" + }, + "peerDependencies": { + "octane": "0.1.21" + } +} diff --git a/packages/octane-table/src/FlexRender.ts b/packages/octane-table/src/FlexRender.ts new file mode 100644 index 0000000000..b4a8a9f71e --- /dev/null +++ b/packages/octane-table/src/FlexRender.ts @@ -0,0 +1,95 @@ +// flexRender / FlexRender — plain TypeScript on purpose. +// +// Neither needs a slot-keyed hook (they call none) nor JSX (`createElement` +// builds the descriptor directly), so this file stays a normal `.ts` module and +// needs no `.tsrx.d.ts` sidecar. +import { createElement } from 'octane' +import type { CellData, RowData, TableFeatures } from '@tanstack/table-core' +import type { OctaneNode } from 'octane' +import type { FlexRenderProps, Renderable } from './types' + +/** + * If rendering headers, cells, or footers with custom markup, use `flexRender` + * instead of `cell.getValue()` or `cell.renderValue()`. + * + * @example flexRender(cell.column.columnDef.cell, cell.getContext()) + * + * Port note: upstream additionally detects class components and + * `react.memo`/`react.forward_ref` exotic objects. Both branches are dead in + * octane — there are no class components or `forwardRef`, and octane's `memo()` + * returns a plain function — so a component is exactly `typeof === 'function'`. + * The descriptor `createElement` returns renders at value position; non-component + * values (strings, numbers, pre-created descriptors) pass through as-is. + */ +export function flexRender( + Comp: Renderable, + props: TProps, +): OctaneNode { + return Comp == null + ? null + : typeof Comp === 'function' + ? createElement(Comp, props) + : Comp +} + +/** + * Simplified component wrapper of {@link flexRender}. Use this utility component + * to render headers, cells, or footers with custom markup. Only one prop + * (`cell`, `header`, or `footer`) may be passed. + * + * @example + * ```tsx + * + * + * + * ``` + * + * This replaces calling `flexRender` directly like this: + * ```tsx + * flexRender(cell.column.columnDef.cell, cell.getContext()) + * flexRender(header.column.columnDef.header, header.getContext()) + * flexRender(footer.column.columnDef.footer, footer.getContext()) + * ``` + */ +export function FlexRender< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, +>(props: FlexRenderProps): OctaneNode { + if ('cell' in props && props.cell) { + const cell = props.cell + const def = cell.column.columnDef + // `columnGroupingFeature` is optional, so these only exist when grouping + // is enabled — probe rather than require the feature in `TFeatures`. + const groupingCell = cell as typeof cell & { + getIsAggregated?: () => boolean + getIsPlaceholder?: () => boolean + } + const groupingDef = def as typeof def & { + aggregatedCell?: typeof def.cell + } + if (groupingCell.getIsAggregated?.()) { + return flexRender( + groupingDef.aggregatedCell ?? def.cell, + cell.getContext(), + ) + } + if (groupingCell.getIsPlaceholder?.()) { + return null + } + return flexRender(def.cell, cell.getContext()) + } + if ('header' in props && props.header) { + return flexRender( + props.header.column.columnDef.header, + props.header.getContext(), + ) + } + if ('footer' in props && props.footer) { + return flexRender( + props.footer.column.columnDef.footer, + props.footer.getContext(), + ) + } + return null +} diff --git a/packages/octane-table/src/Subscribe.tsrx b/packages/octane-table/src/Subscribe.tsrx new file mode 100644 index 0000000000..b8b7135db5 --- /dev/null +++ b/packages/octane-table/src/Subscribe.tsrx @@ -0,0 +1,63 @@ +// Authored in .tsrx because it is a COMPONENT that calls a slot-keyed hook +// (`useSelector`); the compiler assigns its call-site slot. The types live in +// `./types` so the `.tsrx.d.ts` sidecar only restates the signature. +import { shallow, useSelector } from '@tanstack/octane-store'; +import type { SubscribeComponent, SubscribeProps } from './types'; + +// A `@{ … }` body cannot carry preceding overload signatures (TS2384), so the +// implementation takes the union and the exported binding is re-typed with the +// overloaded callable `SubscribeComponent`. Call sites still get per-overload +// inference; only this one function body sees the union. +function SubscribeImpl(props: SubscribeProps) @{ + // Atom and store share the same selection protocol; the union needs a widen + // for TS. `shallow` matches upstream so object-literal selectors don't + // re-render on every notification. + const selected = useSelector( + props.source as Parameters[0], + props.selector as Parameters[1], + { compare: shallow }, + ); + + <> + {typeof props.children === 'function' + ? (props.children as (state: unknown) => unknown)(selected) + : props.children} + +} + +/** + * An octane component that subscribes to table state. + * + * This is useful for opting into state re-renders for specific parts of the + * table state. + * + * For `table.Subscribe` from `useTable`, prefer that API — it is pre-bound to + * `table.store`, so you can omit `source`. + * + * @example + * ```tsx + * // Full store + * state.rowSelection}> + * {(rowSelection) =>
{Object.keys(rowSelection).length as unknown as string}
} + *
+ * ``` + * + * @example + * ```tsx + * // Entire source (atom or store) — no selector + * + * {(rowSelection) =>
} + *
+ * ``` + * + * @example + * ```tsx + * // Project source value (e.g. one row's selection) + * s?.[row.id]}> + * {(selected) => …} + * + * ``` + */ +export const Subscribe = SubscribeImpl as SubscribeComponent; + + diff --git a/packages/octane-table/src/Subscribe.tsrx.d.ts b/packages/octane-table/src/Subscribe.tsrx.d.ts new file mode 100644 index 0000000000..6913fec3ac --- /dev/null +++ b/packages/octane-table/src/Subscribe.tsrx.d.ts @@ -0,0 +1,8 @@ +// Declaration companion for Subscribe.tsrx. +// +// A SPECIFIC module declaration (resolved by relative path), not an ambient +// `declare module '*.tsrx'` — so it types only this module and doesn't pollute a +// consumer's own .tsrx imports. The runtime resolves the real compiled .tsrx. +import type { SubscribeComponent } from './types' + +export declare const Subscribe: SubscribeComponent diff --git a/packages/octane-table/src/createTableHook.tsrx b/packages/octane-table/src/createTableHook.tsrx new file mode 100644 index 0000000000..b5131097ff --- /dev/null +++ b/packages/octane-table/src/createTableHook.tsrx @@ -0,0 +1,466 @@ +// Authored in .tsrx because it renders context Providers and calls slot-keyed +// hooks (`useRef`, `useMemo`). Every type lives in `./types`, so the +// `.tsrx.d.ts` sidecar only restates this one signature. +import { createColumnHelper as coreCreateColumnHelper } from '@tanstack/table-core'; +import { createContext, createElement, useContext, useMemo, useRef } from 'octane'; +import { FlexRender } from './FlexRender'; +import { splitSlot } from './internal'; +import { useTable } from './useTable.tsrx'; +import type { + Cell, + CellData, + Header, + RowData, + TableFeatures, + TableOptions, + TableState, +} from '@tanstack/table-core'; +import type { Context, OctaneNode } from 'octane'; +import type { + AppCellComponent, + AppCellPropsWithSelector, + AppCellPropsWithoutSelector, + AppColumnHelper, + AppHeaderComponent, + AppHeaderPropsWithSelector, + AppHeaderPropsWithoutSelector, + AppOctaneTable, + AppTableComponent, + AppTablePropsWithSelector, + AppTablePropsWithoutSelector, + CreateTableHookOptions, + CreateTableHookResult, + OctaneTable, + TableComponentType, +} from './types'; + +const sharedTableContext = createContext | null>(null); +const sharedCellContext = createContext | null>(null); +const sharedHeaderContext = createContext | null>(null); + +/** + * Creates a custom table hook with pre-bound components for composition. + * + * This is the table equivalent of TanStack Form's `createFormHook`. It lets you: + * - Define features, row models, and default options once, shared across tables + * - Register reusable table, cell, and header components + * - Access table/cell/header instances via context in those components + * - Get a `useAppTable` hook returning a table with App wrapper components + * - Get a `createAppColumnHelper` function pre-bound to your features + * + * @example + * ```tsx + * // hooks/table.ts + * export const { + * useAppTable, + * createAppColumnHelper, + * useTableContext, + * useCellContext, + * useHeaderContext, + * } = createTableHook({ + * features: tableFeatures({ + * rowPaginationFeature, + * rowSortingFeature, + * columnFilteringFeature, + * paginatedRowModel: createPaginatedRowModel(), + * sortedRowModel: createSortedRowModel(), + * filteredRowModel: createFilteredRowModel(), + * sortFns, + * filterFns, + * }), + * tableComponents: { PaginationControls, RowCount }, + * cellComponents: { TextCell, NumberCell }, + * headerComponents: { SortIndicator, ColumnFilter }, + * }) + * + * // components/table-components.tsrx + * function SortIndicator() @{ + * const header = useHeaderContext() + * {(header.column.getIsSorted() || '') as string} + * } + * + * // features/users.tsrx + * function UsersTable({ data }: { data: Array }) @{ + * const table = useAppTable({ columns, data }) + * + * + * + * + * @for (const row of table.getRowModel().rows; key row.id) { + * + * @for (const c of row.getAllCells(); key c.id) { + * + * {(cell) => } + * + * } + * + * } + * + *
+ * + *
+ * } + * ``` + */ +export function createTableHook< + TFeatures extends TableFeatures, + TTableComponents extends Record, + TCellComponents extends Record, + THeaderComponents extends Record, +>({ + tableComponents, + cellComponents, + headerComponents, + tableContext = sharedTableContext as Context>, + cellContext = sharedCellContext as Context>, + headerContext = sharedHeaderContext as Context>, + ...defaultTableOptions +}: CreateTableHookOptions< + TFeatures, + TTableComponents, + TCellComponents, + THeaderComponents +>): CreateTableHookResult { + const TableContext = tableContext as unknown as Context>; + const CellContext = cellContext as unknown as Context>; + const HeaderContext = headerContext as unknown as Context>; + + /** + * Create a column helper pre-bound to the features and components configured + * in this table hook. The cell, header, and footer contexts include pre-bound + * components (e.g. `cell.TextCell`). + * + * @example + * ```tsx + * const columnHelper = createAppColumnHelper() + * + * const columns = [ + * columnHelper.accessor('firstName', { + * header: 'First Name', + * cell: ({ cell }) => , // cell has pre-bound components + * }), + * ] + * ``` + */ + function createAppColumnHelper(): AppColumnHelper< + TFeatures, + TData, + TCellComponents, + THeaderComponents + > { + // The runtime implementation is the same — components are attached at + // render time. This cast provides the enhanced column-def types. + return coreCreateColumnHelper() as unknown as AppColumnHelper< + TFeatures, + TData, + TCellComponents, + THeaderComponents + >; + } + + /** + * Access the table instance from within an `AppTable` wrapper. Use this in + * custom `tableComponents` passed to `createTableHook`. `TFeatures` is already + * known from the `createTableHook` call. + * + * @example + * ```tsx + * function PaginationControls() @{ + * const table = useTableContext() + * s.pagination}> + * {(pagination) => ( + * + * )} + * + * } + * ``` + */ + function useTableContext< + TData extends RowData = RowData, + TSelected = TableState, + >(): AppOctaneTable< + TFeatures, + TData, + TSelected, + TTableComponents, + TCellComponents, + THeaderComponents + > { + const table = useContext(TableContext); + + if (!table) { + throw new Error('`useTableContext` must be used within an `AppTable` component. ' + + 'Make sure your component is wrapped with `...`.'); + } + + // The value provided by `` is the extended table (the App* + // wrapper components and `tableComponents` are Object.assign-ed onto the + // same instance `useAppTable` returns), so this asserts the runtime shape. + return table as unknown as AppOctaneTable< + TFeatures, + TData, + TSelected, + TTableComponents, + TCellComponents, + THeaderComponents + >; + } + + /** + * Access the cell instance from within an `AppCell` wrapper. Use this in + * custom `cellComponents` passed to `createTableHook`. + * + * @example + * ```tsx + * function TextCell() @{ + * const cell = useCellContext() + * {cell.getValue()} + * } + * ``` + */ + function useCellContext() { + const cell = useContext(CellContext); + + if (!cell) { + throw new Error('`useCellContext` must be used within an `AppCell` component. ' + + 'Make sure your component is wrapped with `...`.'); + } + + // `` Object.assign-es `cellComponents` and `FlexRender` onto + // the same cell instance it provides, so this asserts the runtime shape. + return cell as unknown as + Cell & TCellComponents & { + FlexRender: () => OctaneNode; + }; + } + + /** + * Access the header instance from within an `AppHeader` or `AppFooter` + * wrapper. Use this in custom `headerComponents` passed to `createTableHook`. + * + * @example + * ```tsx + * function SortIndicator() @{ + * const header = useHeaderContext() + * {(header.column.getIsSorted() || '') as string} + * } + * ``` + */ + function useHeaderContext() { + const header = useContext(HeaderContext); + + if (!header) { + throw new Error('`useHeaderContext` must be used within an `AppHeader` or `AppFooter` component.'); + } + + // `` / `` Object.assign `headerComponents` + // and `FlexRender` onto the same header instance they provide. + return header as unknown as + Header & THeaderComponents & { + FlexRender: () => OctaneNode; + }; + } + + /** Context-aware FlexRender for cells — reads the cell from context. */ + function CellFlexRender() { + const cell = useCellContext(); + return ; + } + + /** Context-aware FlexRender for headers — reads the header from context. */ + function HeaderFlexRender() { + const header = useHeaderContext(); + return ; + } + + /** Context-aware FlexRender for footers — reads the header from context. */ + function FooterFlexRender() { + const header = useHeaderContext(); + return ; + } + + /** + * Enhanced `useTable` that returns a table with App wrapper components and + * pre-bound `tableComponents` attached directly to the table object. + * + * Default options from `createTableHook` are merged with the options passed + * here; options passed here take precedence. + */ + function useAppTable>( + tableOptions: Omit, 'features'>, + // `selector` is optional, so the compiler-injected call-site slot would + // land in it. Split it off before reading the selector — see ./internal. + ...rest: [selector?: (state: TableState) => TSelected, slot?: symbol] + ): AppOctaneTable< + TFeatures, + TData, + TSelected, + TTableComponents, + TCellComponents, + THeaderComponents + > { + const [user] = splitSlot(rest); + const selector = user[0] as ((state: TableState) => TSelected) | undefined; + + // Merge default options with provided options (provided takes precedence). + const table = useTable( + { ...defaultTableOptions, ...tableOptions } as TableOptions, + selector, + ); + + // `useTable` returns a fresh `table` reference every render. The App wrapper + // components below must NOT close over that reference, or they would be + // recreated each render and octane would remount their entire subtree on + // every state update (e.g. a controlled input in a toolbar would lose focus + // on each keystroke). Instead the components stay stable (created once) and + // read the current table from a ref refreshed every render. + const tableRef = useRef(table); + tableRef.current = table; + + // AppTable — root wrapper providing table context, with optional Subscribe. + const AppTable = useMemo(() => { + function AppTableImpl( + props: | AppTablePropsWithoutSelector + | AppTablePropsWithSelector, + ) { + const { children, selector: appTableSelector } = props as any; + const currentTable = tableRef.current; + + // Built in setup with `createElement` so the template stays a single + // renderable hole rather than branching JSX inside the Provider. + const body = + appTableSelector + ? createElement(currentTable.Subscribe as any, { + selector: appTableSelector, + children: (state: TAppTableSelected) => children(state), + }) + : children; + + return {body}; + } + return AppTableImpl as AppTableComponent; + }, []); + + // AppCell — cell context, pre-bound cellComponents, optional Subscribe. + const AppCell = useMemo(() => { + function AppCellImpl( + props: | AppCellPropsWithoutSelector + | AppCellPropsWithSelector, + ) { + const { cell, children, selector: appCellSelector } = props as any; + const currentTable = tableRef.current; + const extendedCell = Object.assign(cell, { + FlexRender: CellFlexRender, + ...cellComponents, + }); + + const body = + appCellSelector + ? createElement(currentTable.Subscribe as any, { + selector: appCellSelector, + children: (state: TAppCellSelected) => children(extendedCell, state), + }) + : children(extendedCell); + + return {body}; + } + return AppCellImpl as AppCellComponent; + }, []); + + // AppHeader — header context, pre-bound headerComponents, optional Subscribe. + const AppHeader = useMemo(() => { + function AppHeaderImpl( + props: | AppHeaderPropsWithoutSelector + | AppHeaderPropsWithSelector< + TFeatures, + TData, + TValue, + THeaderComponents, + TAppHeaderSelected + >, + ) { + const { header, children, selector: appHeaderSelector } = props as any; + const currentTable = tableRef.current; + const extendedHeader = Object.assign(header, { + FlexRender: HeaderFlexRender, + ...headerComponents, + }); + + const body = + appHeaderSelector + ? createElement(currentTable.Subscribe as any, { + selector: appHeaderSelector, + children: (state: TAppHeaderSelected) => children(extendedHeader, state), + }) + : children(extendedHeader); + + return {body}; + } + return AppHeaderImpl as AppHeaderComponent; + }, []); + + // AppFooter — same as AppHeader (footers use the Header type), but its + // context-bound FlexRender renders `columnDef.footer`. + const AppFooter = useMemo(() => { + function AppFooterImpl( + props: | AppHeaderPropsWithoutSelector + | AppHeaderPropsWithSelector< + TFeatures, + TData, + TValue, + THeaderComponents, + TAppFooterSelected + >, + ) { + const { header, children, selector: appFooterSelector } = props as any; + const currentTable = tableRef.current; + const extendedHeader = Object.assign(header, { + FlexRender: FooterFlexRender, + ...headerComponents, + }); + + const body = + appFooterSelector + ? createElement(currentTable.Subscribe as any, { + selector: appFooterSelector, + children: (state: TAppFooterSelected) => children(extendedHeader, state), + }) + : children(extendedHeader); + + return {body}; + } + return AppFooterImpl as AppHeaderComponent; + }, []); + + // Combine everything into the extended table API. + const extendedTable = useMemo(() => { + return Object.assign(table, { + AppTable, + AppCell, + AppHeader, + AppFooter, + ...tableComponents, + }) as AppOctaneTable< + TFeatures, + TData, + TSelected, + TTableComponents, + TCellComponents, + THeaderComponents + >; + }, [table, AppTable, AppCell, AppHeader, AppFooter]); + + return extendedTable; + } + + return { + appFeatures: defaultTableOptions.features as TFeatures, + createAppColumnHelper, + useAppTable, + useTableContext, + useCellContext, + useHeaderContext, + }; +} + + diff --git a/packages/octane-table/src/createTableHook.tsrx.d.ts b/packages/octane-table/src/createTableHook.tsrx.d.ts new file mode 100644 index 0000000000..9eec97cdea --- /dev/null +++ b/packages/octane-table/src/createTableHook.tsrx.d.ts @@ -0,0 +1,30 @@ +// Declaration companion for createTableHook.tsrx. +// +// A SPECIFIC module declaration (resolved by relative path), not an ambient +// `declare module '*.tsrx'` — so it types only this module and doesn't pollute a +// consumer's own .tsrx imports. The runtime resolves the real compiled .tsrx. +import type { TableFeatures } from '@tanstack/table-core' +import type { + CreateTableHookOptions, + CreateTableHookResult, + TableComponentType, +} from './types' + +export declare function createTableHook< + TFeatures extends TableFeatures, + const TTableComponents extends Record, + const TCellComponents extends Record, + const THeaderComponents extends Record, +>( + options: CreateTableHookOptions< + TFeatures, + TTableComponents, + TCellComponents, + THeaderComponents + >, +): CreateTableHookResult< + TFeatures, + TTableComponents, + TCellComponents, + THeaderComponents +> diff --git a/packages/octane-table/src/createTableHookContexts.ts b/packages/octane-table/src/createTableHookContexts.ts new file mode 100644 index 0000000000..f5a5d68368 --- /dev/null +++ b/packages/octane-table/src/createTableHookContexts.ts @@ -0,0 +1,143 @@ +// Scoped table/cell/header contexts. +// +// Plain TypeScript: `createContext` is not a hook, and `useContext` is exempt +// from octane's call-site slot keying (it is context-identity keyed), so this +// module needs neither compilation to `.tsrx` nor a sidecar. +import { createContext, useContext } from 'octane' +import type { + Cell, + CellData, + Header, + RowData, + TableFeatures, +} from '@tanstack/table-core' +import type { Context } from 'octane' +import type { OctaneTable, TableHookContexts } from './types' + +/** + * Creates a fresh, scoped set of table/cell/header contexts (plus matching + * context hooks) that you can pass into `createTableHook`. This mirrors + * TanStack Form's `createFormHookContexts`. + * + * You usually do NOT need this: by default `createTableHook` wires its + * `AppTable`/`AppCell`/`AppHeader` providers to a shared module-scoped context, + * and you read it with the `useTableContext`/`useCellContext`/`useHeaderContext` + * hooks returned from `createTableHook`. Reach for `createTableHookContexts` + * when you need an *isolated* context, e.g. when nesting one table inside + * another and a consumer would otherwise read the wrong (nearest) provider. + * + * Type-safety note: the hooks returned here are typed with `TFeatures` only. + * They do NOT know the component maps you register in `createTableHook` + * (`tableComponents`/`cellComponents`/`headerComponents`), because those are + * defined later. For the richest types (the `App*` components and your + * registered components attached), prefer the `use*Context` hooks returned from + * your `createTableHook` call. The hooks here are the escape hatch for reading + * context from a module that does not / cannot import the `createTableHook` + * result. + * + * @example + * ```ts + * // scoped-table-context.ts + * export const { + * tableContext, + * cellContext, + * headerContext, + * useTableContext, + * useCellContext, + * useHeaderContext, + * } = createTableHookContexts() + * + * // table.ts + * export const { useAppTable } = createTableHook({ + * features, + * tableContext, // <- pass the scoped contexts so the providers use them + * cellContext, + * headerContext, + * tableComponents: { PaginationControls }, + * }) + * ``` + */ +export function createTableHookContexts< + TFeatures extends TableFeatures, + TData extends RowData = RowData, +>(): TableHookContexts { + // Fresh contexts per call: this is what makes them scoped/isolated. They are + // intentionally loosely typed (`any` features); the `TFeatures` typing is + // layered on by the hooks below. + const tableContext = createContext | null>(null) + const cellContext = createContext | null>(null) + const headerContext = createContext | null>(null) + + /** + * Access the table instance from within an `AppTable` wrapper bound to these + * scoped contexts. `TFeatures` is known; the registered component maps are + * not (see {@link createTableHookContexts}). + */ + function useTableContext(): OctaneTable< + TFeatures, + TTableData + > { + const table = useContext(tableContext) + + if (!table) { + throw new Error( + '`useTableContext` must be used within an `AppTable` component. ' + + 'Make sure your component is wrapped with `...`.', + ) + } + + return table as unknown as OctaneTable + } + + /** + * Access the cell instance from within an `AppCell` wrapper bound to these + * scoped contexts. + */ + function useCellContext(): Cell< + TFeatures, + any, + TValue + > { + const cell = useContext(cellContext) + + if (!cell) { + throw new Error( + '`useCellContext` must be used within an `AppCell` component. ' + + 'Make sure your component is wrapped with `...`.', + ) + } + + return cell as unknown as Cell + } + + /** + * Access the header instance from within an `AppHeader` or `AppFooter` + * wrapper bound to these scoped contexts. + */ + function useHeaderContext(): Header< + TFeatures, + any, + TValue + > { + const header = useContext(headerContext) + + if (!header) { + throw new Error( + '`useHeaderContext` must be used within an `AppHeader` or `AppFooter` component.', + ) + } + + return header as unknown as Header + } + + return { + // Re-typed without `| null` so they drop straight into `createTableHook`'s + // `tableContext`/`cellContext`/`headerContext` options. + tableContext: tableContext as unknown as Context>, + cellContext: cellContext as unknown as Context>, + headerContext: headerContext as unknown as Context>, + useTableContext, + useCellContext, + useHeaderContext, + } +} diff --git a/packages/octane-table/src/experimental-worker-plugin.ts b/packages/octane-table/src/experimental-worker-plugin.ts new file mode 100644 index 0000000000..fe6241f2ec --- /dev/null +++ b/packages/octane-table/src/experimental-worker-plugin.ts @@ -0,0 +1 @@ +export * from '@tanstack/table-core/experimental-worker-plugin' diff --git a/packages/octane-table/src/flex-render.ts b/packages/octane-table/src/flex-render.ts new file mode 100644 index 0000000000..07f3e0486d --- /dev/null +++ b/packages/octane-table/src/flex-render.ts @@ -0,0 +1 @@ +export * from './FlexRender' diff --git a/packages/octane-table/src/index.d.ts b/packages/octane-table/src/index.d.ts new file mode 100644 index 0000000000..1dac9576c4 --- /dev/null +++ b/packages/octane-table/src/index.d.ts @@ -0,0 +1,8 @@ +export * from '@tanstack/table-core' + +export * from './FlexRender' +export * from './Subscribe.tsrx' +export * from './createTableHook.tsrx' +export * from './createTableHookContexts' +export * from './types' +export * from './useTable.tsrx' diff --git a/packages/octane-table/src/index.ts b/packages/octane-table/src/index.ts new file mode 100644 index 0000000000..02b6c07284 --- /dev/null +++ b/packages/octane-table/src/index.ts @@ -0,0 +1,19 @@ +// @tanstack/octane-table — TanStack Table v9 for the Octane renderer. +// +// TanStack Table v9 separates a framework-agnostic core (`@tanstack/table-core`: +// `constructTable`, tree-shakeable features, and TanStack Store atoms behind +// every state slice) from a thin framework adapter. This package re-exports the +// core verbatim and ports the adapter — `useTable`, `Subscribe`, `FlexRender`, +// `createTableHook`, `createTableHookContexts` — onto octane. +// +// Upstream's `useLegacyTable` v8-compat layer is deliberately NOT ported: it +// exists to migrate existing React v8 codebases, which an octane binding has +// none of. Octane code targets the v9 `useTable` API directly. +export * from '@tanstack/table-core' + +export * from './FlexRender' +export * from './Subscribe.tsrx' +export * from './createTableHook.tsrx' +export * from './createTableHookContexts' +export * from './types' +export * from './useTable.tsrx' diff --git a/packages/octane-table/src/internal.ts b/packages/octane-table/src/internal.ts new file mode 100644 index 0000000000..740c8a1d90 --- /dev/null +++ b/packages/octane-table/src/internal.ts @@ -0,0 +1,22 @@ +// Hook-slot mechanics shared by the binding's exported hooks (the same helper +// as @tanstack/octane-store and the other Octane adapters). +// +// The octane compiler appends a per-call-site `Symbol` slot as the LAST +// argument of every `use*` call. A hook whose own signature ends in an OPTIONAL +// parameter must therefore split that slot off before reading its arguments, or +// the slot lands in the optional parameter — e.g. `useTable(options)` compiles +// to `useTable(options, slot)`, and the slot would be read as the `selector`. +// +// The compiled hook body still gets its internal slots from the compiler's +// ambient `withSlot` scope, so the split only has to DISCARD the trailing +// symbol; it never needs to be threaded onward. + +/** + * Splits the compiler-injected trailing slot off a hook's runtime args, + * returning the user args (everything before it) and the slot. + */ +export function splitSlot(args: Array): [Array, symbol | undefined] { + const tail = args[args.length - 1] + const slot = typeof tail === 'symbol' ? tail : undefined + return [slot !== undefined ? args.slice(0, -1) : args, slot] +} diff --git a/packages/octane-table/src/reactivity.ts b/packages/octane-table/src/reactivity.ts new file mode 100644 index 0000000000..c5710fd514 --- /dev/null +++ b/packages/octane-table/src/reactivity.ts @@ -0,0 +1,17 @@ +import { batch, createAtom } from '@tanstack/octane-store' +import { renderPhaseReactivity } from '@tanstack/table-core/reactivity' +import type { RenderPhaseReactivityBindings } from '@tanstack/table-core/reactivity' + +export type OctaneTableReactivityBindings = RenderPhaseReactivityBindings + +/** + * Creates the table-core reactivity bindings used by the Octane adapter. + * + * The render-phase preset supplies live readonly-atom facades and commit-aware + * external-state publication. Store primitives come from the Octane adapter so + * table-created atoms, user-provided atoms, batching, and subscriptions all use + * the same TanStack Store module instance. + */ +export function octaneReactivity(): OctaneTableReactivityBindings { + return renderPhaseReactivity({ createAtom, batch }) +} diff --git a/packages/octane-table/src/static-functions.ts b/packages/octane-table/src/static-functions.ts new file mode 100644 index 0000000000..e0cb951593 --- /dev/null +++ b/packages/octane-table/src/static-functions.ts @@ -0,0 +1 @@ +export * from '@tanstack/table-core/static-functions' diff --git a/packages/octane-table/src/types.ts b/packages/octane-table/src/types.ts new file mode 100644 index 0000000000..43b89bd645 --- /dev/null +++ b/packages/octane-table/src/types.ts @@ -0,0 +1,887 @@ +// Every type in the binding's public surface lives here, in plain TypeScript. +// +// The runtime lives in `.tsrx` files, which ship a hand-written `.tsrx.d.ts` +// sidecar so consumers on plain `tsc`/`tsgo` still get types. Keeping the types +// in a real `.ts` module means those sidecars only restate function signatures — +// the type definitions themselves are checked once, here, and imported by both +// the `.tsrx` implementation and its sidecar. +import type { + Atom, + ReadonlyAtom, + ReadonlyStore, + Store, +} from '@tanstack/octane-store' +import type { + AccessorFn, + AccessorFnColumnDef, + AccessorKeyColumnDef, + Cell, + CellContext, + CellData, + Column, + ColumnDef, + NoInfer as CoreNoInfer, + DeepKeys, + DeepValue, + DisplayColumnDef, + GroupColumnDef, + Header, + IdentifiedColumnDef, + Row, + RowData, + Table, + TableFeatures, + TableOptions, + TableState, +} from '@tanstack/table-core' +import type { + ComponentBody, + Context, + ElementDescriptor, + OctaneNode, +} from 'octane' + +// ============================================================================= +// Renderables +// ============================================================================= + +/** + * Anything a `columnDef.cell`/`header`/`footer` slot may hold: an octane + * component, an already-created element descriptor, or a primitive that + * renders as text. + */ +export type Renderable = + | ComponentBody + | ElementDescriptor + | string + | number + | boolean + | null + | undefined + +/** + * A component in a `createTableHook` component registry. + * + * Structural on purpose (octane components are plain functions), so registries + * accept components declared in `.tsrx`, `.tsx`, or plain `.ts` alike. + */ +export type TableComponentType = (props: TProps) => OctaneNode + +// ============================================================================= +// FlexRender +// ============================================================================= + +/** + * Props for the {@link FlexRender} component. Exactly one of `cell`, `header`, + * or `footer` may be passed. + */ +export type FlexRenderProps< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, +> = + | { cell: Cell; header?: never; footer?: never } + | { header: Header; cell?: never; footer?: never } + | { footer: Header; cell?: never; header?: never } + +// ============================================================================= +// Subscribe +// ============================================================================= + +/** Any atom or store that `Subscribe` can read and subscribe to. */ +export type SubscribeSource = + | Atom + | ReadonlyAtom + | Store + | ReadonlyStore + +/** + * A non-render-prop child accepted by Subscribe. This stays concrete instead + * of using `OctaneNode` (currently `unknown`) so function children retain + * contextual typing. + */ +export type SubscribeStaticChild = + | ElementDescriptor + | string + | number + | boolean + | null + | undefined + | ReadonlyArray + +/** + * Subscribe to `table.store` (full table state). The selector receives the full + * {@link TableState}. + */ +export interface SubscribePropsWithStore< + TFeatures extends TableFeatures, + TSelected, +> { + source: SubscribeSource> + /** + * Select from full table state. Re-renders when the selected value changes + * (shallow compare). + * + * Required in store mode so you never accidentally subscribe to the whole + * store without an explicit projection. + */ + selector: (state: TableState) => TSelected + children: ((state: TSelected) => OctaneNode) | SubscribeStaticChild +} + +/** + * Subscribe to the full value of a source (e.g. `table.atoms.rowSelection` or + * `table.optionsStore`). Omitting `selector` is equivalent to the identity + * selector — children receive `TSourceValue`. + */ +export interface SubscribePropsWithSourceIdentity { + source: SubscribeSource + selector?: undefined + children: ((state: TSourceValue) => OctaneNode) | SubscribeStaticChild +} + +/** + * Subscribe to a projected value from a source (atom or store). The selector + * receives the source value; children receive the projected `TSelected`. + */ +export interface SubscribePropsWithSourceWithSelector { + source: SubscribeSource + selector: (state: TSourceValue) => TSelected + children: ((state: TSelected) => OctaneNode) | SubscribeStaticChild +} + +/** + * Subscribe to a single source — atom or store (identity or projected). Prefer + * {@link SubscribePropsWithSourceIdentity} or + * {@link SubscribePropsWithSourceWithSelector} for clearer inference when + * `selector` is omitted. + */ +export type SubscribePropsWithSource = + | SubscribePropsWithSourceIdentity + | SubscribePropsWithSourceWithSelector + +export type SubscribeProps< + TFeatures extends TableFeatures, + TSelected = unknown, + TSourceValue = unknown, +> = + | SubscribePropsWithStore + | SubscribePropsWithSourceIdentity + | SubscribePropsWithSourceWithSelector + +/** + * The call signature of the standalone `Subscribe` component. + * + * Declared as an overloaded callable type rather than `function` overloads + * because the implementation is authored in `.tsrx`: a `@{ … }` body cannot + * carry preceding overload signatures (TS2384). Overload *resolution* is what + * matters at the JSX call site, and a callable type provides exactly that. + * + * The **source** overloads come first so `TSourceValue` is inferred from + * `source`; the identity overload (no `selector`) is separate so children + * receive `TSourceValue` rather than `unknown`. + */ +export interface SubscribeComponent { + ( + props: SubscribePropsWithSourceIdentity, + ): OctaneNode + ( + props: SubscribePropsWithSourceWithSelector, + ): OctaneNode + ( + props: SubscribePropsWithStore, + ): OctaneNode +} + +// ============================================================================= +// useTable +// ============================================================================= + +/** + * The table instance returned by {@link useTable}: the framework-agnostic + * `Table` from table-core, plus octane's `Subscribe`/`FlexRender` components + * and the selected `state`. + */ +export type OctaneTable< + TFeatures extends TableFeatures, + TData extends RowData, + TSelected = TableState, +> = Omit, 'store'> & { + /** + * @deprecated Prefer `table.state` for render reads, + * `table.atoms..get()` for slice snapshots, or + * `table.Subscribe` / `useSelector(table.store, selector)` for explicit + * subscriptions. `table.store.state` is a current-value snapshot and is easy + * to misuse in render code. + */ + readonly store: Table['store'] + /** + * An octane component that subscribes to the table state. + * + * This is useful for opting into state re-renders for specific parts of the + * table state. + * + * Pass `source` to subscribe to a single atom or store (e.g. + * `table.atoms.rowSelection` or `table.optionsStore`) instead of the full + * `table.store`. + * + * @example + * ```tsx + * ({ rowSelection: state.rowSelection })}> + * {({ rowSelection }) =>
{Object.keys(rowSelection).length as string}
} + *
+ * ``` + * + * @example + * ```tsx + * + * {(rowSelection) =>
} + *
+ * ``` + */ + /** + * Overloads (not a single union) so `selector` callbacks get correct + * contextual types in JSX; a union of two `selector` signatures degrades to + * implicit `any`. + * + * Source **without** `selector` is a separate overload so children receive + * `TSourceValue` (identity projection). If `selector` were optional on one + * overload, `TSubSelected` would default to `unknown` instead of inferring + * from the source. + * + * The **source** overloads are listed first so `TSourceValue` is inferred + * from `source`. + */ + Subscribe: { + (props: { + source: SubscribeSource + selector?: undefined + children: ((state: TSourceValue) => OctaneNode) | SubscribeStaticChild + }): OctaneNode + (props: { + source: SubscribeSource + selector: (state: TSourceValue) => TSubSelected + children: ((state: TSubSelected) => OctaneNode) | SubscribeStaticChild + }): OctaneNode + ( + props: Omit, 'source'>, + ): OctaneNode + } + /** + * An octane component that renders headers, cells, or footers with custom + * markup. Use this utility component instead of manually calling + * `flexRender`. + * + * @example + * ```tsx + * + * + * + * ``` + * + * This replaces calling `flexRender` directly like this: + * ```tsx + * flexRender(cell.column.columnDef.cell, cell.getContext()) + * flexRender(header.column.columnDef.header, header.getContext()) + * flexRender(footer.column.columnDef.footer, footer.getContext()) + * ``` + */ + FlexRender: ( + props: FlexRenderProps, + ) => OctaneNode + /** + * The selected state of the table. This state may not match the structure of + * the full table state because it is selected by the selector function that + * you pass as the 2nd argument to `useTable`. + * + * @example + * const table = useTable(options, (state) => ({ globalFilter: state.globalFilter })) + * + * table.state.globalFilter + */ + readonly state: Readonly +} + +// ============================================================================= +// createTableHookContexts +// ============================================================================= + +/** + * The object returned by `createTableHookContexts`: three scoped octane + * contexts plus matching context hooks. + */ +export interface TableHookContexts< + TFeatures extends TableFeatures, + TData extends RowData, +> { + tableContext: Context> + cellContext: Context> + headerContext: Context> + useTableContext: () => OctaneTable< + TFeatures, + TTableData + > + useCellContext: () => Cell< + TFeatures, + any, + TValue + > + useHeaderContext: () => Header< + TFeatures, + any, + TValue + > +} + +// ============================================================================= +// createTableHook — enhanced contexts with pre-bound components +// ============================================================================= + +/** + * Enhanced CellContext with pre-bound cell components. + * The `cell` property includes the registered cellComponents. + */ +export interface AppCellContext< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + TCellComponents extends Record, +> { + cell: Cell & + TCellComponents & { FlexRender: () => OctaneNode } + column: Column + getValue: CellContext['getValue'] + renderValue: CellContext['renderValue'] + row: Row + table: Table +} + +/** + * Enhanced HeaderContext with pre-bound header components. + * The `header` property includes the registered headerComponents. + */ +export interface AppHeaderContext< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + THeaderComponents extends Record, +> { + column: Column + header: Header & + THeaderComponents & { FlexRender: () => OctaneNode } + table: Table +} + +/** + * Template type for column definitions that can be a string or a function. + */ +export type AppColumnDefTemplate = + | string + | ((props: TProps) => any) + +/** + * Enhanced column definition base with pre-bound components in + * cell/header/footer contexts. + */ +export type AppColumnDefBase< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + TCellComponents extends Record, + THeaderComponents extends Record, +> = Omit< + IdentifiedColumnDef, + 'cell' | 'header' | 'footer' +> & { + cell?: AppColumnDefTemplate< + AppCellContext + > + header?: AppColumnDefTemplate< + AppHeaderContext + > + footer?: AppColumnDefTemplate< + AppHeaderContext + > +} + +/** + * Enhanced display column definition with pre-bound components. + */ +export type AppDisplayColumnDef< + TFeatures extends TableFeatures, + TData extends RowData, + TCellComponents extends Record, + THeaderComponents extends Record, +> = Omit< + DisplayColumnDef, + 'cell' | 'header' | 'footer' +> & { + cell?: AppColumnDefTemplate< + AppCellContext + > + header?: AppColumnDefTemplate< + AppHeaderContext + > + footer?: AppColumnDefTemplate< + AppHeaderContext + > +} + +/** + * Enhanced group column definition with pre-bound components. + */ +export type AppGroupColumnDef< + TFeatures extends TableFeatures, + TData extends RowData, + TCellComponents extends Record, + THeaderComponents extends Record, +> = Omit< + GroupColumnDef, + 'cell' | 'header' | 'footer' | 'columns' +> & { + cell?: AppColumnDefTemplate< + AppCellContext + > + header?: AppColumnDefTemplate< + AppHeaderContext + > + footer?: AppColumnDefTemplate< + AppHeaderContext + > + columns?: Array> +} + +/** + * Enhanced column helper with pre-bound components in cell/header/footer + * contexts. This enables TypeScript to know about the registered components + * when defining columns. + */ +export interface AppColumnHelper< + TFeatures extends TableFeatures, + TData extends RowData, + TCellComponents extends Record, + THeaderComponents extends Record, +> { + /** + * Creates a data column definition with an accessor key or function. + * The cell, header, and footer contexts include pre-bound components. + */ + accessor: < + TAccessor extends AccessorFn | DeepKeys, + TValue extends TAccessor extends AccessorFn + ? TReturn + : TAccessor extends DeepKeys + ? DeepValue + : never, + >( + accessor: TAccessor, + column: TAccessor extends AccessorFn + ? AppColumnDefBase< + TFeatures, + TData, + TValue, + TCellComponents, + THeaderComponents + > & { + id: string + } + : AppColumnDefBase< + TFeatures, + TData, + TValue, + TCellComponents, + THeaderComponents + >, + ) => TAccessor extends AccessorFn + ? AccessorFnColumnDef + : AccessorKeyColumnDef + + /** + * Wraps an array of column definitions to preserve each column's individual + * TValue type. + */ + columns: >>( + columns: [...TColumns], + ) => Array> & [...TColumns] + + /** + * Creates a display column definition for non-data columns. + * The cell, header, and footer contexts include pre-bound components. + */ + display: ( + column: AppDisplayColumnDef< + TFeatures, + TData, + TCellComponents, + THeaderComponents + >, + ) => DisplayColumnDef + + /** + * Creates a group column definition with nested child columns. + * The cell, header, and footer contexts include pre-bound components. + */ + group: ( + column: AppGroupColumnDef< + TFeatures, + TData, + TCellComponents, + THeaderComponents + >, + ) => GroupColumnDef +} + +/** + * Options for creating a table hook with pre-bound components and default table + * options. Extends all TableOptions except + * `columns` | `data` | `store` | `state` | `initialState`. + */ +export type CreateTableHookOptions< + TFeatures extends TableFeatures, + TTableComponents extends Record, + TCellComponents extends Record, + THeaderComponents extends Record, +> = Omit< + TableOptions, + 'columns' | 'data' | 'store' | 'state' | 'initialState' +> & { + /** + * Table-level components that need access to the table instance. + * These are available directly on the table object returned by useAppTable. + * Use `useTableContext()` inside these components. + * @example { PaginationControls, GlobalFilter, RowCount } + */ + tableComponents?: TTableComponents + /** + * Cell-level components that need access to the cell instance. + * These are available on the cell object passed to AppCell's children. + * Use `useCellContext()` inside these components. + * @example { TextCell, NumberCell, DateCell, CurrencyCell } + */ + cellComponents?: TCellComponents + /** + * Header-level components that need access to the header instance. + * These are available on the header object passed to AppHeader/AppFooter's + * children. Use `useHeaderContext()` inside these components. + * @example { SortIndicator, ColumnFilter, ResizeHandle } + */ + headerComponents?: THeaderComponents + /** + * A custom octane context for the table instance (read with `useContext` + * inside your `tableComponents`). Optional: defaults to a shared + * module-scoped context. Only pass your own (created via `createContext`) + * when you need to isolate this table's context from other tables, e.g. when + * nesting one table inside another. + */ + tableContext?: Context> + /** + * A custom octane context for the cell instance, used inside your + * `cellComponents`. + * @see {@link CreateTableHookOptions.tableContext} + */ + cellContext?: Context> + /** + * A custom octane context for the header instance, used inside your + * `headerComponents` (and footer components). + * @see {@link CreateTableHookOptions.tableContext} + */ + headerContext?: Context> +} + +/** Props for AppTable component — without selector. */ +export interface AppTablePropsWithoutSelector { + children: OctaneNode + selector?: never +} + +/** Props for AppTable component — with selector. */ +export interface AppTablePropsWithSelector< + TFeatures extends TableFeatures, + TSelected, +> { + children: (state: TSelected) => OctaneNode + selector: (state: TableState) => TSelected +} + +/** Props for AppCell component — without selector. */ +export interface AppCellPropsWithoutSelector< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + TCellComponents extends Record, +> { + cell: Cell + children: ( + cell: Cell & + TCellComponents & { FlexRender: () => OctaneNode }, + ) => OctaneNode + selector?: never +} + +/** Props for AppCell component — with selector. */ +export interface AppCellPropsWithSelector< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + TCellComponents extends Record, + TSelected, +> { + cell: Cell + children: ( + cell: Cell & + TCellComponents & { FlexRender: () => OctaneNode }, + state: TSelected, + ) => OctaneNode + selector: (state: TableState) => TSelected +} + +/** Props for AppHeader/AppFooter component — without selector. */ +export interface AppHeaderPropsWithoutSelector< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + THeaderComponents extends Record, +> { + header: Header + children: ( + header: Header & + THeaderComponents & { FlexRender: () => OctaneNode }, + ) => OctaneNode + selector?: never +} + +/** Props for AppHeader/AppFooter component — with selector. */ +export interface AppHeaderPropsWithSelector< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, + THeaderComponents extends Record, + TSelected, +> { + header: Header + children: ( + header: Header & + THeaderComponents & { FlexRender: () => OctaneNode }, + state: TSelected, + ) => OctaneNode + selector: (state: TableState) => TSelected +} + +/** + * Component type for AppCell — wraps a cell and provides cell context with + * optional Subscribe. + */ +export interface AppCellComponent< + TFeatures extends TableFeatures, + TData extends RowData, + TCellComponents extends Record, +> { + ( + props: AppCellPropsWithoutSelector< + TFeatures, + TData, + TValue, + TCellComponents + >, + ): OctaneNode + ( + props: AppCellPropsWithSelector< + TFeatures, + TData, + TValue, + TCellComponents, + TSelected + >, + ): OctaneNode +} + +/** + * Component type for AppHeader/AppFooter — wraps a header and provides header + * context with optional Subscribe. + */ +export interface AppHeaderComponent< + TFeatures extends TableFeatures, + TData extends RowData, + THeaderComponents extends Record, +> { + ( + props: AppHeaderPropsWithoutSelector< + TFeatures, + TData, + TValue, + THeaderComponents + >, + ): OctaneNode + ( + props: AppHeaderPropsWithSelector< + TFeatures, + TData, + TValue, + THeaderComponents, + TSelected + >, + ): OctaneNode +} + +/** Component type for AppTable — root wrapper with optional Subscribe. */ +export interface AppTableComponent { + (props: AppTablePropsWithoutSelector): OctaneNode + ( + props: AppTablePropsWithSelector, + ): OctaneNode +} + +/** + * Extended table API returned by `useAppTable` with all App wrapper components. + */ +export type AppOctaneTable< + TFeatures extends TableFeatures, + TData extends RowData, + TSelected, + TTableComponents extends Record, + TCellComponents extends Record, + THeaderComponents extends Record, +> = OctaneTable & + CoreNoInfer & { + /** + * Root wrapper component that provides table context with optional + * Subscribe. + * + * @example + * ```tsx + * // Without selector — children is a renderable + * + * …
+ *
+ * + * // With selector — children receives selected state + * s.pagination}> + * {(pagination) =>
{pagination.pageIndex as unknown as string}
} + *
+ * ``` + */ + AppTable: AppTableComponent + /** + * Wraps a cell and provides cell context with pre-bound cellComponents. + * Optionally accepts a selector for Subscribe functionality. + * + * @example + * ```tsx + * + * {(c) => } + * + * ``` + */ + AppCell: AppCellComponent> + /** + * Wraps a header and provides header context with pre-bound + * headerComponents. Optionally accepts a selector for Subscribe + * functionality. + * + * @example + * ```tsx + * + * {(h) => } + * + * ``` + */ + AppHeader: AppHeaderComponent< + TFeatures, + TData, + CoreNoInfer + > + /** + * Wraps a footer and provides header context with pre-bound + * headerComponents. + * + * @example + * ```tsx + * + * {(f) => } + * + * ``` + */ + AppFooter: AppHeaderComponent< + TFeatures, + TData, + CoreNoInfer + > + } + +export interface CreateTableHookResult< + TFeatures extends TableFeatures, + TTableComponents extends Record, + TCellComponents extends Record, + THeaderComponents extends Record, +> { + /** The features object that was passed to `createTableHook`. */ + appFeatures: TFeatures + /** + * A column helper pre-bound to `TFeatures` and the registered components, so + * the cell/header/footer render props expose the bound components. + */ + createAppColumnHelper: () => AppColumnHelper< + TFeatures, + TData, + TCellComponents, + THeaderComponents + > + /** + * Creates a table with the `App*` wrapper components and registered + * `tableComponents` attached. `TData` is inferred from the `data` option. + */ + useAppTable: >( + tableOptions: Omit, 'features'>, + selector?: (state: TableState) => TSelected, + ) => AppOctaneTable< + TFeatures, + TData, + TSelected, + TTableComponents, + TCellComponents, + THeaderComponents + > + /** + * Reads the table provided by the nearest ``. This is the + * same extended instance `useAppTable` returns, so the `App*` components and + * your `tableComponents` are available on it. + * + * Pass `TSelected` to match the selector you gave `useAppTable`, so + * `table.state` is typed as the selected slice. It cannot be inferred + * automatically (context does not carry the provider's generics), so it + * defaults to the full table state, which is correct for the common case of + * `useAppTable` without a selector. + */ + useTableContext: < + TData extends RowData = RowData, + TSelected = TableState, + >() => AppOctaneTable< + TFeatures, + TData, + TSelected, + TTableComponents, + TCellComponents, + THeaderComponents + > + /** + * Reads the cell provided by the nearest ``, extended with + * your `cellComponents` and a context-bound `FlexRender`. + */ + useCellContext: () => Cell< + TFeatures, + any, + TValue + > & + TCellComponents & { FlexRender: () => OctaneNode } + /** + * Reads the header provided by the nearest `` / + * ``, extended with your `headerComponents` and a + * context-bound `FlexRender`. + */ + useHeaderContext: () => Header< + TFeatures, + any, + TValue + > & + THeaderComponents & { FlexRender: () => OctaneNode } +} diff --git a/packages/octane-table/src/useTable.tsrx b/packages/octane-table/src/useTable.tsrx new file mode 100644 index 0000000000..41986c2f70 --- /dev/null +++ b/packages/octane-table/src/useTable.tsrx @@ -0,0 +1,95 @@ +import { shallow, useSelector } from '@tanstack/octane-store' +import { constructTable } from '@tanstack/table-core' +import { createRenderPhaseSource } from '@tanstack/table-core/reactivity' +import { + table_publishExternalState, + table_setOptions, +} from '@tanstack/table-core/static-functions' +import { useLayoutEffect, useMemo, useState } from 'octane' +import { FlexRender } from './FlexRender' +import { splitSlot } from './internal' +import { octaneReactivity } from './reactivity' +import { Subscribe } from './Subscribe.tsrx' +import type { + RowData, + Table, + TableFeatures, + TableOptions, + TableState, +} from '@tanstack/table-core' +import type { OctaneTable } from './types' + +/** + * Creates an Octane table instance backed by TanStack Store atoms. + * + * The optional selector projects from `table.store`; the selected value is + * exposed on `table.state` and compared shallowly for owner rerenders. + */ +export function useTable< + TFeatures extends TableFeatures, + TData extends RowData, + TSelected = TableState, +>( + tableOptions: TableOptions, + ...rest: [ + selector?: (state: TableState) => TSelected, + slot?: symbol, + ] +): OctaneTable { + const [user] = splitSlot(rest) + const selector = user[0] as + | ((state: TableState) => TSelected) + | undefined + + const [{ table, rootSource }] = useState(() => { + const tableInstance = constructTable({ + ...tableOptions, + features: { + coreReactivityFeature: octaneReactivity(), + ...tableOptions.features, + }, + }) as unknown as OctaneTable + + tableInstance.Subscribe = ((props: any) => ( + + )) as OctaneTable['Subscribe'] + tableInstance.FlexRender = FlexRender + + return { + table: tableInstance, + rootSource: createRenderPhaseSource>( + tableInstance.store, + shallow, + ), + } + }) + + const coreTable = table as unknown as Table + + table_setOptions( + coreTable, + (prev) => ({ + ...prev, + ...tableOptions, + }), + { syncExternalState: false }, + ) + + const controlledState = coreTable.options.state + const renderSnapshot = rootSource.get() + const state = useSelector(rootSource, selector, { compare: shallow }) + + useLayoutEffect(() => { + rootSource.markCommitted(renderSnapshot) + table_publishExternalState(coreTable, controlledState ?? null, shallow) + }, null) + + return useMemo( + () => ({ + ...table, + options: tableOptions, + state, + }), + [table, tableOptions, state], + ) as OctaneTable +} diff --git a/packages/octane-table/src/useTable.tsrx.d.ts b/packages/octane-table/src/useTable.tsrx.d.ts new file mode 100644 index 0000000000..d9840c14f9 --- /dev/null +++ b/packages/octane-table/src/useTable.tsrx.d.ts @@ -0,0 +1,21 @@ +// Declaration companion for useTable.tsrx. +// +// A SPECIFIC module declaration (resolved by relative path), not an ambient +// `declare module '*.tsrx'` — so it types only this module and doesn't pollute a +// consumer's own .tsrx imports. The runtime resolves the real compiled .tsrx. +import type { + RowData, + TableFeatures, + TableOptions, + TableState, +} from '@tanstack/table-core' +import type { OctaneTable } from './types' + +export declare function useTable< + TFeatures extends TableFeatures, + TData extends RowData, + TSelected = TableState, +>( + tableOptions: TableOptions, + selector?: (state: TableState) => TSelected, +): OctaneTable diff --git a/packages/octane-table/tests/_fixtures/adapter-reactivity.tsrx b/packages/octane-table/tests/_fixtures/adapter-reactivity.tsrx new file mode 100644 index 0000000000..1803a4b2e5 --- /dev/null +++ b/packages/octane-table/tests/_fixtures/adapter-reactivity.tsrx @@ -0,0 +1,183 @@ +import { createAtom } from '@tanstack/octane-store' +import { + Subscribe, + stockFeatures, + useTable, +} from '@tanstack/octane-table' +import { + Suspense, + use, + useState, +} from 'octane' +import type { + ColumnDef, + OctaneTable, + PaginationState, + RowSelectionState, +} from '@tanstack/octane-table' + +type Data = { id: string; title: string } + +const data: Array = [ + { id: '1', title: 'First' }, + { id: '2', title: 'Second' }, +] +const columns: Array> = [ + { id: 'id', accessorKey: 'id', header: 'Id' }, + { id: 'title', accessorKey: 'title', header: 'Title' }, +] + +type TableInstance = OctaneTable + +export const phaseCapture: { + table?: TableInstance + rendering: boolean + renders: number + notifications: Array<{ duringRender: boolean; pageIndex: number }> +} = { + rendering: false, + renders: 0, + notifications: [], +} + +export function PhaseHarness() @{ + phaseCapture.rendering = true + phaseCapture.renders++ + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 1, + }) + const table = useTable( + { + features: stockFeatures, + data, + columns, + state: { pagination }, + onPaginationChange: setPagination, + }, + () => null, + ) + phaseCapture.table = table + phaseCapture.rendering = false + +
+ {String(pagination.pageIndex)} + state.pagination.pageIndex}> + {(pageIndex: number) => {String(pageIndex)}} + + +
+} + +export const externalSelectionAtom = createAtom({ + external: true, +}) +export const externalCapture: { table?: TableInstance } = {} + +export function ExternalAtomHarness() @{ + const table = useTable( + { + features: stockFeatures, + data, + columns, + state: { rowSelection: { controlled: true } }, + atoms: { rowSelection: externalSelectionAtom }, + }, + () => null, + ) + externalCapture.table = table + +
+ + {(selection: RowSelectionState) => + {JSON.stringify(selection)} + } + + +
+} + +export const sourceA = createAtom({ value: 'A0' }) +export const sourceB = createAtom({ value: 'B0' }) +export const sourceCapture = { renders: 0 } + +export function SourceSwitchHarness() @{ + const [useB, setUseB] = useState(false) + const source = useB ? sourceB : sourceA + +
+ + {(value: { value: string }) => { + sourceCapture.renders++ + return {value.value} + }} + + +
+} + +const never = new Promise(() => {}) +export const suspendedCapture: { + table?: TableInstance + setPagination?: (state: PaginationState) => void +} = {} + +function SuspendingOwner() { + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 1, + }) + const table = useTable( + { + features: stockFeatures, + data, + columns, + state: { pagination }, + onPaginationChange: setPagination, + }, + () => null, + ) + suspendedCapture.table = table + suspendedCapture.setPagination = setPagination + + if (pagination.pageIndex === 1) use(never) + + return {String(pagination.pageIndex)} +} + +export function SuspenseHarness() @{ + Suspended}> + + +} + +export const ownershipCapture: { table?: TableInstance } = {} + +export function OwnershipHarness() @{ + const [controlled, setControlled] = useState(true) + const pagination: PaginationState = { pageIndex: 2, pageSize: 1 } + const table = useTable( + { + features: stockFeatures, + data, + columns, + state: controlled ? { pagination } : {}, + }, + () => null, + ) + ownershipCapture.table = table + +
+ state.pagination.pageIndex}> + {(pageIndex: number) => {String(pageIndex)}} + + + + +
+} diff --git a/packages/octane-table/tests/_fixtures/app-table.tsrx b/packages/octane-table/tests/_fixtures/app-table.tsrx new file mode 100644 index 0000000000..c023702473 --- /dev/null +++ b/packages/octane-table/tests/_fixtures/app-table.tsrx @@ -0,0 +1,135 @@ +import { + createSortedRowModel, + createTableHook, + rowSortingFeature, + sortFns, + tableFeatures, +} from '@tanstack/octane-table'; + +// createTableHook fixtures — the composition layer: registered table/cell/header +// components read their instance from context, and the App* wrappers provide it. + +export const captured: { table?: any; cellCtx: unknown[] } = { cellCtx: [] }; + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}); + +const data = [ + { firstName: 'tanner', age: 29 }, + { firstName: 'derek', age: 45 }, + { firstName: 'joe', age: 42 }, +]; + +// Registered components resolve their instance through the scoped contexts that +// createTableHook wires into the App* providers. +function TextCell() @{ + const cell = useCellContext(); + captured.cellCtx.push(cell); + {'t:' + cell.getValue()} +} + +function SortIndicator() @{ + const header = useHeaderContext(); + {(header.column.getIsSorted() || 'none') as string} +} + +function RowCount() @{ + const table = useTableContext(); + {'rows=' + table.getRowModel().rows.length} +} + +export const { + useAppTable, + createAppColumnHelper, + useTableContext, + useCellContext, + useHeaderContext, +} = createTableHook({ + features, + tableComponents: { RowCount }, + cellComponents: { TextCell }, + headerComponents: { SortIndicator }, +}); + +const columnHelper = createAppColumnHelper<(typeof data)[number]>(); + +const columns = [ + columnHelper.accessor('firstName', { header: 'First', footer: 'f-first' }), + columnHelper.accessor('age', { header: 'Age', footer: 'f-age' }), +]; + +export function AppTableFixture() @{ + const table = useAppTable({ data, columns: columns as any }); + captured.table = table; + +
+ + state.sorting}> + {(sorting: Array) => {'n=' + (sorting?.length ?? 0)}} + + + + @for (const hg of table.getHeaderGroups(); key hg.id) { + + @for (const h of hg.headers; key h.id) { + + {(header) => } + + } + + } + + + @for (const row of table.getRowModel().rows; key row.id) { + + @for (const c of row.getAllCells(); key c.id) { + + {(cell) => } + + } + + } + + + @for (const fg of table.getFooterGroups(); key fg.id) { + + @for (const f of fg.headers; key f.id) { + + {(footer) => } + + } + + } + +
+ + +
+ +
+ +
+
+
+} + +// AppTable with a selector: children become a render prop over selected state. +export function AppTableWithSelector() @{ + const table = useAppTable({ data, columns: columns as any }); + state.sorting}> + {(sorting) =>
{'sorted=' + (sorting?.length ?? 0)}
} +
+} + +// Reading table context OUTSIDE an AppTable provider must throw a helpful error. +export function OrphanTableConsumer() @{ +
+ +
+} diff --git a/packages/octane-table/tests/_fixtures/features.tsrx b/packages/octane-table/tests/_fixtures/features.tsrx new file mode 100644 index 0000000000..ff544bf8c4 --- /dev/null +++ b/packages/octane-table/tests/_fixtures/features.tsrx @@ -0,0 +1,305 @@ +import { + aggregationFns, + columnFacetingFeature, + columnFilteringFeature, + columnGroupingFeature, + columnVisibilityFeature, + createExpandedRowModel, + createFacetedRowModel, + createFacetedUniqueValues, + createFilteredRowModel, + createGroupedRowModel, + createPaginatedRowModel, + filterFns, + flexRender, + globalFilteringFeature, + rowAggregationFeature, + rowExpandingFeature, + rowPaginationFeature, + rowSelectionFeature, + tableFeatures, + useTable, +} from '@tanstack/octane-table'; + +// Feature-smoke fixtures — each table-core feature wired through the octane +// render path. Data is module-level and deterministic. +// +// v9 opts into features explicitly per table (they are tree-shakeable), so each +// fixture below declares only the features it exercises. + +export const captured: { table?: any } = {}; + +const people = [ + { firstName: 'tanner', lastName: 'linsley', age: 29, status: 'single' }, + { firstName: 'derek', lastName: 'perkins', age: 45, status: 'complicated' }, + { firstName: 'joe', lastName: 'bergevin', age: 42, status: 'single' }, + { firstName: 'kevin', lastName: 'vandy', age: 27, status: 'single' }, + { firstName: 'ryan', lastName: 'carniato', age: 38, status: 'complicated' }, +]; + +const nameCols = [ + { accessorKey: 'firstName', header: 'First' }, + { accessorKey: 'age', header: 'Age' }, + { accessorKey: 'status', header: 'Status' }, +]; + +// ── Column + global filtering (+ faceting, which needs the filtered model). ── +const filterFeatures = tableFeatures({ + columnFilteringFeature, + globalFilteringFeature, + columnFacetingFeature, + filteredRowModel: createFilteredRowModel(), + facetedRowModel: createFacetedRowModel(), + facetedUniqueValues: createFacetedUniqueValues(), + filterFns, +}); + +export function FilterTable(props: { global?: boolean }) @{ + const table = useTable({ + features: filterFeatures, + data: people, + columns: nameCols, + }); + captured.table = table; +
+ @if (props.global) { + table.setGlobalFilter((e.target as HTMLInputElement).value)} + /> + } @else { + table.getColumn('firstName')!.setFilterValue( + (e.target as HTMLInputElement).value, + )} + /> + } +
    + @for (const row of table.getRowModel().rows; key row.id) { +
  • {row.original.firstName as string}
  • + } +
+
+} + +// ── Pagination: 25 rows, pageSize 10, button-driven. ── +const manyRows = Array.from( + { length: 25 }, + (_, i) => ({ + firstName: 'p' + String(i).padStart(2, '0'), + age: i, + }), +); + +const paginationFeatures = tableFeatures({ + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), +}); + +export function PaginationTable() @{ + const table = useTable({ + features: paginationFeatures, + data: manyRows, + columns: [ + { accessorKey: 'firstName', header: 'First' }, + ], + }); + captured.table = table; +
+ {table.state.pagination.pageIndex + '/' + table.getPageCount()} + + + +
    + @for (const row of table.getRowModel().rows; key row.id) { +
  • {row.original.firstName as string}
  • + } +
+
+} + +// ── Row selection: per-row checkbox + header select-all, optional predicate. ── +const selectionFeatures = tableFeatures({ rowSelectionFeature }); + +export function SelectionTable(props: { predicated?: boolean }) @{ + const table = useTable({ + features: selectionFeatures, + data: people, + columns: nameCols, + enableRowSelection: props.predicated ? (row: any) => row.original.age > 40 : true, + }); + captured.table = table; +
+ + {String(table.getSelectedRowModel().rows.length)} +
    + @for (const row of table.getRowModel().rows; key row.id) { +
  • + + {row.original.firstName as string} +
  • + } +
+
+} + +// ── Column visibility: grouped header (colSpan shrinks) + per-leaf toggles. ── +const groupedCols = [ + { + id: 'name', + header: 'Name', + columns: [ + { accessorKey: 'firstName', header: 'First' }, + { accessorKey: 'lastName', header: 'Last' }, + ], + }, + { accessorKey: 'age', header: 'Age' }, +]; + +const visibilityFeatures = tableFeatures({ columnVisibilityFeature }); + +export function VisibilityTable() @{ + const table = useTable({ + features: visibilityFeatures, + data: people, + columns: groupedCols, + }); + captured.table = table; +
+ @for (const col of table.getAllLeafColumns(); key col.id) { + + } + + + @for (const hg of table.getHeaderGroups(); key hg.id) { + + @for (const header of hg.headers; key header.id) { + + } + + } + + + @for (const row of table.getRowModel().rows; key row.id) { + + @for (const cell of row.getVisibleCells(); key cell.id) { + + } + + } + +
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+} + +// ── Expanding: nested subRows, expander per row, depth surfaced as data attr. ── +const tree = [ + { + name: 'root-a', + subRows: [{ name: 'a-1' }, { name: 'a-2', subRows: [{ name: 'a-2-x' }] }], + }, + { name: 'root-b' }, +]; + +const expandFeatures = tableFeatures({ + rowExpandingFeature, + expandedRowModel: createExpandedRowModel(), +}); + +export function ExpandTable() @{ + const table = useTable({ + features: expandFeatures, + data: tree, + columns: [ + { accessorKey: 'name', header: 'Name' }, + ], + getSubRows: (row: any) => row.subRows, + }); + captured.table = table; +
    + @for (const row of table.getRowModel().rows; key row.id) { +
  • + @if (row.getCanExpand()) { + + } + {row.original.name as string} +
  • + } +
+} + +// ── Grouping + aggregation: grouped by status, age SUM in the aggregated row. ── +// v9 splits v8's single grouping concern: `columnGroupingFeature` groups rows and +// `rowAggregationFeature` aggregates their values, so both are opted into here. +const groupFeatures = tableFeatures({ + columnGroupingFeature, + rowAggregationFeature, + rowExpandingFeature, + groupedRowModel: createGroupedRowModel(), + expandedRowModel: createExpandedRowModel(), + aggregationFns, +}); + +export function GroupTable() @{ + const table = useTable({ + features: groupFeatures, + data: people, + columns: [ + { accessorKey: 'status', header: 'Status' }, + { + accessorKey: 'age', + header: 'Age', + aggregationFn: 'sum', + aggregatedCell: (info: any) => 'sum:' + info.getValue(), + }, + ], + initialState: { grouping: ['status'], expanded: true }, + }); + captured.table = table; +
    + @for (const row of table.getRowModel().rows; key row.id) { +
  • + @for (const cell of row.getAllCells(); key cell.id) { + + {cell.getIsAggregated() + ? flexRender( + cell.column.columnDef.aggregatedCell ?? cell.column.columnDef.cell, + cell.getContext(), + ) + : cell.getIsPlaceholder() + ? null + : flexRender(cell.column.columnDef.cell, cell.getContext())} + + } +
  • + } +
+} + diff --git a/packages/octane-table/tests/_fixtures/flex-render.tsrx b/packages/octane-table/tests/_fixtures/flex-render.tsrx new file mode 100644 index 0000000000..d214e7b958 --- /dev/null +++ b/packages/octane-table/tests/_fixtures/flex-render.tsrx @@ -0,0 +1,73 @@ +import { memo } from 'octane'; +import { flexRender, tableFeatures, useTable } from '@tanstack/octane-table'; + +// flexRender triage fixtures: string header, render-fn cell, component cell +// (props captured), pre-created descriptor passthrough, undefined renderer, +// and an octane memo()-wrapped component (plain function — the branch upstream +// needed exotic-component sniffing for). + +export const cellProps: unknown[] = []; + +const features = tableFeatures({}); + +function NameCell(props: any) @{ + cellProps.push(props); + {'nc:' + props.getValue()} +} + +// NOTE: declaration + memo() — a function EXPRESSION with an @{ } body is not +// a supported syntax position for the compiler's component lowering. +function AgeCell(props: any) @{ + {'mc:' + props.getValue()} +} +const MemoAgeCell = memo(AgeCell); + +// A PRE-CREATED element descriptor (not a function): flexRender's non-component +// branch must pass it through as-is. +const staticCell = static; + +const data = [ + { firstName: 'tanner', lastName: 'linsley', age: 29 }, + { firstName: 'derek', lastName: 'perkins', age: 40 }, +]; + +const columns = [ + { accessorKey: 'firstName', header: 'First Name', cell: NameCell }, + { accessorKey: 'lastName', header: (info: any) => 'hdr:' + info.column.id, cell: staticCell }, + { accessorKey: 'age', header: undefined, cell: MemoAgeCell }, +]; + +export function FlexTable() @{ + const table = useTable({ + features, + data, + columns: columns as any, + }); + + + @for (const hg of table.getHeaderGroups(); key hg.id) { + + @for (const header of hg.headers; key header.id) { + + } + + } + + + @for (const row of table.getRowModel().rows; key row.id) { + + @for (const cell of row.getAllCells(); key cell.id) { + + } + + } + +
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} +
{flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )}
+} diff --git a/packages/octane-table/tests/_fixtures/ssr-table.tsrx b/packages/octane-table/tests/_fixtures/ssr-table.tsrx new file mode 100644 index 0000000000..4e18afb405 --- /dev/null +++ b/packages/octane-table/tests/_fixtures/ssr-table.tsrx @@ -0,0 +1,50 @@ +import { + createColumnHelper, + flexRender, + stockFeatures, + useTable, +} from '@tanstack/octane-table' + +type Person = { name: string; age: number } + +const data: Array = [{ name: 'Ada', age: 37 }] +const columnHelper = createColumnHelper() +const columns = columnHelper.columns([ + columnHelper.accessor('name', { + header: () => Person, + cell: (info) => {String(info.getValue())}, + }), + columnHelper.accessor('age', { + header: 'Age', + cell: (info) => String(info.getValue()), + }), +]) + +export function SsrTable() @{ + const table = useTable({ + features: stockFeatures, + data, + columns, + state: { + pagination: { pageIndex: 2, pageSize: 10 }, + }, + }) + const row = table.getRowModel().rows[0]! + const header = table.getHeaderGroups()[0]!.headers[0]! + +
+ {String(table.state.pagination.pageIndex)} +
+ {flexRender(header.column.columnDef.header, header.getContext())} +
+ + + + @for (const cell of row.getAllCells(); key cell.id) { + + } + + +
+
+} diff --git a/packages/octane-table/tests/_fixtures/table-basic.tsrx b/packages/octane-table/tests/_fixtures/table-basic.tsrx new file mode 100644 index 0000000000..3698d1191d --- /dev/null +++ b/packages/octane-table/tests/_fixtures/table-basic.tsrx @@ -0,0 +1,226 @@ +import { useState } from 'octane'; +import { + createSortedRowModel, + flexRender, + rowSortingFeature, + sortFns, + tableFeatures, + useTable, +} from '@tanstack/octane-table'; + +// Core-wiring fixtures. Data/columns are module-level (referentially stable) +// so table-core's row-model memoization isn't perturbed and render-count +// probes measure only the state wiring. + +export const renders = { basic: 0, sorting: 0, controlled: 0, selected: 0 }; +export const captured: { + table?: unknown; + tables: unknown[]; + sortingStates: unknown[]; + selectedStates: unknown[]; +} = { tables: [], sortingStates: [], selectedStates: [] }; + +export const defaultData = [ + { firstName: 'tanner', lastName: 'linsley', age: 29 }, + { firstName: 'derek', lastName: 'perkins', age: 40 }, + { firstName: 'joe', lastName: 'bergevin', age: 45 }, +]; +export const altData = [ + { firstName: 'kevin', lastName: 'vandy', age: 27 }, + { firstName: 'tanner', lastName: 'linsley', age: 29 }, +]; + +const defaultColumns = [ + { + accessorKey: 'firstName', + header: 'First Name', + footer: 'fn-footer', + cell: (info: any) => info.getValue(), + }, + { accessorKey: 'lastName', header: () => 'Last Name', cell: (info: any) => info.getValue() }, + { accessorKey: 'age', header: 'Age' }, +]; +const altColumns = [ + { accessorKey: 'age', header: 'Only Age' }, +]; + +// v9 always builds the core row model, so a plain table opts into no features. +const coreFeatures = tableFeatures({}); + +const sortingFeatures = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), + sortFns, +}); + +// Full-markup table (thead/tbody/tfoot, all through flexRender), used both +// standalone and by the swap harness below. +export function BasicTable(props: { data?: any; columns?: any }) @{ + renders.basic++; + const table = useTable({ + features: coreFeatures, + data: props.data ?? defaultData, + columns: props.columns ?? defaultColumns, + }); + captured.table = table; + captured.tables.push(table); + + + @for (const hg of table.getHeaderGroups(); key hg.id) { + + @for (const header of hg.headers; key header.id) { + + } + + } + + + @for (const row of table.getRowModel().rows; key row.id) { + + @for (const cell of row.getAllCells(); key cell.id) { + + } + + } + + + @for (const fg of table.getFooterGroups(); key fg.id) { + + @for (const header of fg.headers; key header.id) { + + } + + } + +
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} +
{flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )}
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.footer, header.getContext())} +
+} + +// Parent-driven data/columns swaps (props propagate through the render-phase +// setOptions) + an unrelated-state bump for the instance-identity probe. +export function SwapApp() @{ + const [alt, setAlt] = useState(false); + const [altCols, setAltCols] = useState(false); + const [bump, setBump] = useState(0); +
+ + + + +
+} + +// Uncontrolled sorting + render-count probe + double-toggle (functional +// updater) button. Deliberately NO `onSortingChange`: in v9 the feature's +// DEFAULT handler is `makeStateUpdater('sorting', table)`, which writes the +// sorting atom, so supplying one REPLACES that writer and makes the slice +// controlled (see ControlledSortingTable). +export function SortingTable() @{ + renders.sorting++; + const table = useTable({ + features: sortingFeatures, + data: defaultData, + columns: defaultColumns, + }); + captured.table = table; +
+ + + @for (const hg of table.getHeaderGroups(); key hg.id) { + + @for (const header of hg.headers; key header.id) { + + } + + } + + + @for (const row of table.getRowModel().rows; key row.id) { + + @for (const cell of row.getAllCells(); key cell.id) { + + } + + } + +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ asc: ' A', desc: ' D' }[header.column.getIsSorted() as string] ?? ''} +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+} + +// Partially-controlled sorting: the parent owns `sorting` (state + feature +// handler), everything else stays uncontrolled. +export function ControlledSortingTable() @{ + renders.controlled++; + const [sorting, setSorting] = useState([] as any); + captured.sortingStates.push(sorting); + const table = useTable({ + features: sortingFeatures, + data: defaultData, + columns: defaultColumns, + state: { sorting }, + onSortingChange: setSorting, + }); + + + @for (const hg of table.getHeaderGroups(); key hg.id) { + + @for (const header of hg.headers; key header.id) { + + } + + } + + + @for (const row of table.getRowModel().rows; key row.id) { + + @for (const cell of row.getAllCells(); key cell.id) { + + } + + } + +
{flexRender(header.column.columnDef.header, header.getContext())}
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+} + +// v9-only adapter surface: the second `useTable` argument projects from +// `table.store`, and that projection — not the whole table state — is what +// lands on `table.state`. +export function SelectorTable() @{ + renders.selected++; + const table = useTable( + { + features: sortingFeatures, + data: defaultData, + columns: defaultColumns, + }, + (state) => ({ sortCount: state.sorting?.length ?? 0 }), + ); + captured.table = table; + captured.selectedStates.push(table.state); +
+ {'n=' + table.state.sortCount} + +
+} + diff --git a/packages/octane-table/tests/_helpers.ts b/packages/octane-table/tests/_helpers.ts new file mode 100644 index 0000000000..dcbedb7816 --- /dev/null +++ b/packages/octane-table/tests/_helpers.ts @@ -0,0 +1,74 @@ +import { + createRoot, + delegateEvents, + drainPassiveEffects, + flushSync, +} from 'octane' +import type { ComponentBody, Root } from 'octane' + +delegateEvents(['click', 'input', 'change']) + +interface MountResult { + container: HTMLElement + root: Root + unmount: () => void + click: (selector: string) => void + find: (selector: string) => Element + findAll: (selector: string) => Array +} + +export function mount( + body: ComponentBody, + props?: TProps, +): MountResult { + const container = document.createElement('div') + document.body.appendChild(container) + const root = createRoot(container) + + try { + root.render(body, props) + flushSync(() => {}) + } catch (error) { + try { + root.unmount() + } finally { + container.remove() + } + throw error + } + + return { + container, + root, + unmount() { + root.unmount() + container.remove() + }, + click(selector) { + const element = container.querySelector(selector) + if (!element) throw new Error(`No element matching ${selector}`) + flushSync(() => { + if (typeof (element as HTMLElement).click === 'function') { + ;(element as HTMLElement).click() + } else { + element.dispatchEvent( + new MouseEvent('click', { bubbles: true, cancelable: true }), + ) + } + }) + }, + find(selector) { + const element = container.querySelector(selector) + if (!element) throw new Error(`No element matching ${selector}`) + return element + }, + findAll(selector) { + return Array.from(container.querySelectorAll(selector)) + }, + } +} + +export function nextPaint(): Promise { + drainPassiveEffects() + return Promise.resolve() +} diff --git a/packages/octane-table/tests/conformance/adapter-reactivity.test.ts b/packages/octane-table/tests/conformance/adapter-reactivity.test.ts new file mode 100644 index 0000000000..31298c9a99 --- /dev/null +++ b/packages/octane-table/tests/conformance/adapter-reactivity.test.ts @@ -0,0 +1,180 @@ +import { flushSync } from 'octane' +import { beforeEach, describe, expect, it } from 'vitest' +import { mount, nextPaint } from '../_helpers' +import { + ExternalAtomHarness, + OwnershipHarness, + PhaseHarness, + SourceSwitchHarness, + SuspenseHarness, + externalCapture, + externalSelectionAtom, + ownershipCapture, + phaseCapture, + sourceA, + sourceB, + sourceCapture, + suspendedCapture, +} from '../_fixtures/adapter-reactivity.tsrx' + +async function flush() { + for (let index = 0; index < 3; index++) { + await Promise.resolve() + await nextPaint() + } +} + +beforeEach(() => { + phaseCapture.table = undefined + phaseCapture.rendering = false + phaseCapture.renders = 0 + phaseCapture.notifications.length = 0 + externalCapture.table = undefined + externalSelectionAtom.set({ external: true }) + sourceA.set({ value: 'A0' }) + sourceB.set({ value: 'B0' }) + sourceCapture.renders = 0 + suspendedCapture.table = undefined + suspendedCapture.setPagination = undefined + ownershipCapture.table = undefined +}) + +describe('render-phase state publication', () => { + it('commits owner and isolated controlled updates in the discrete event', () => { + const view = mount(PhaseHarness, {}) + + ;(view.find('#phase-next') as HTMLButtonElement).click() + + expect(view.find('#phase-page').textContent).toBe('1') + expect(view.find('#phase-island').textContent).toBe('1') + view.unmount() + }) + + it('publishes controlled state only after render and avoids a redundant owner render', async () => { + const view = mount(PhaseHarness, {}) + await flush() + const table = phaseCapture.table! + const initialRenders = phaseCapture.renders + const subscription = table.store.subscribe((state) => { + phaseCapture.notifications.push({ + duringRender: phaseCapture.rendering, + pageIndex: state.pagination.pageIndex, + }) + }) + + view.click('#phase-next') + await flush() + + expect(view.find('#phase-page').textContent).toBe('1') + expect(view.find('#phase-island').textContent).toBe('1') + expect(phaseCapture.renders).toBe(initialRenders + 1) + expect(phaseCapture.notifications.length).toBeGreaterThan(0) + expect(phaseCapture.notifications.at(-1)).toEqual({ + duringRender: false, + pageIndex: 1, + }) + expect( + phaseCapture.notifications.every((event) => !event.duringRender), + ).toBe(true) + + subscription.unsubscribe() + view.unmount() + }) + + it('does not publish controlled state from suspended abandoned work', async () => { + const view = mount(SuspenseHarness, {}) + await flush() + const table = suspendedCapture.table! + const notifications: Array = [] + const subscription = table.store.subscribe((state) => { + notifications.push(state.pagination.pageIndex) + }) + + flushSync(() => { + suspendedCapture.setPagination!({ pageIndex: 1, pageSize: 1 }) + }) + await flush() + + expect(view.find('#suspended').textContent).toBe('Suspended') + // Live readonly facades may expose the staged speculative options, but the + // owned base atom is the publication boundary and must remain committed. + expect(table.store.get().pagination.pageIndex).toBe(1) + expect(table.baseAtoms.pagination.get().pageIndex).toBe(0) + expect(notifications).toEqual([]) + + subscription.unsubscribe() + view.unmount() + }) +}) + +describe('ownership and isolated subscriptions', () => { + it('gives external atoms precedence and routes table writes to them', async () => { + const view = mount(ExternalAtomHarness, {}) + await flush() + + expect(view.find('#external-selection').textContent).toBe( + '{"external":true}', + ) + view.click('#external-write') + await flush() + + expect(externalSelectionAtom.get()).toEqual({ written: true }) + expect(view.find('#external-selection').textContent).toBe( + '{"written":true}', + ) + view.unmount() + }) + + it('releases and reacquires ownership of a controlled slice', async () => { + const view = mount(OwnershipHarness, {}) + await flush() + expect(view.find('#ownership-page').textContent).toBe('2') + + view.click('#release') + await flush() + view.click('#set-internal') + await flush() + expect(view.find('#ownership-page').textContent).toBe('4') + + view.click('#reacquire') + await flush() + expect(view.find('#ownership-page').textContent).toBe('2') + view.unmount() + }) + + it('moves a Subscribe instance when its source identity changes', async () => { + const view = mount(SourceSwitchHarness, {}) + await flush() + expect(view.find('#source-value').textContent).toBe('A0') + + flushSync(() => sourceA.set({ value: 'A1' })) + await flush() + expect(view.find('#source-value').textContent).toBe('A1') + + view.click('#switch-source') + await flush() + expect(view.find('#source-value').textContent).toBe('B0') + const rendersAfterSwitch = sourceCapture.renders + + flushSync(() => sourceA.set({ value: 'A2' })) + await flush() + expect(view.find('#source-value').textContent).toBe('B0') + expect(sourceCapture.renders).toBe(rendersAfterSwitch) + + flushSync(() => sourceB.set({ value: 'B1' })) + await flush() + expect(view.find('#source-value').textContent).toBe('B1') + view.unmount() + }) + + it('removes isolated subscriptions on unmount', async () => { + const view = mount(SourceSwitchHarness, {}) + await flush() + const rendersBeforeUnmount = sourceCapture.renders + view.unmount() + + flushSync(() => sourceA.set({ value: 'after' })) + await flush() + expect(sourceCapture.renders).toBe(rendersBeforeUnmount) + }) +}) diff --git a/packages/octane-table/tests/conformance/app-table.test.ts b/packages/octane-table/tests/conformance/app-table.test.ts new file mode 100644 index 0000000000..e035861f64 --- /dev/null +++ b/packages/octane-table/tests/conformance/app-table.test.ts @@ -0,0 +1,96 @@ +/** + * createTableHook conformance — the composition layer on top of `useTable`: + * App* wrappers provide the table/cell/header through octane context, and the + * registered component maps are attached to the instances handed to children. + */ +import { describe, it, expect, beforeEach } from 'vitest' +import { mount, nextPaint } from '../_helpers' +import { + AppTableFixture, + AppTableWithSelector, + OrphanTableConsumer, + captured, +} from '../_fixtures/app-table.tsrx' + +async function flush() { + for (let i = 0; i < 4; i++) { + await new Promise((r) => setTimeout(r, 0)) + await nextPaint() + } +} + +beforeEach(() => { + captured.table = undefined + captured.cellCtx.length = 0 +}) + +describe('createTableHook', () => { + it('renders registered cell/header/table components bound to their context', async () => { + const r = mount(AppTableFixture, {}) + await flush() + + // tableComponents are attached to the table instance itself. + expect(r.find('#row-count').textContent).toBe('rows=3') + + // cellComponents resolve the cell through cellContext. + expect(r.findAll('.txt').map((n) => n.textContent)).toEqual([ + 't:tanner', + 't:29', + 't:derek', + 't:45', + 't:joe', + 't:42', + ]) + + // headerComponents resolve the header through headerContext, and the + // context-bound FlexRender renders columnDef.header. + expect(r.find('#a-th-firstName').textContent).toBe('Firstnone') + expect(r.findAll('.sort').map((n) => n.textContent)).toEqual([ + 'none', + 'none', + ]) + + // AppFooter's context-bound FlexRender renders columnDef.footer, not header. + expect(r.findAll('.a-foot').map((n) => n.textContent)).toEqual([ + 'f-first', + 'f-age', + ]) + r.unmount() + }) + + it('passes the real cell instance (not a copy) to cellComponents', async () => { + const r = mount(AppTableFixture, {}) + await flush() + const table = captured.table + const firstCell = table.getRowModel().rows[0].getAllCells()[0] + expect(captured.cellCtx[0]).toBe(firstCell) + r.unmount() + }) + + it('re-renders a table.Subscribe child when its selected slice changes', async () => { + const r = mount(AppTableFixture, {}) + await flush() + expect(r.find('#sort-n').textContent).toBe('n=0') + + r.click('#a-th-firstName') // sort asc + await flush() + expect(r.find('#sort-n').textContent).toBe('n=1') + expect(r.find('#a-th-firstName').textContent).toBe('Firstasc') + // The sorted order reached the DOM through the AppCell children. + expect(r.findAll('.txt')[0].textContent).toBe('t:derek') + r.unmount() + }) + + it('AppTable with a selector renders children as a render prop', async () => { + const r = mount(AppTableWithSelector, {}) + await flush() + expect(r.find('#sel-out').textContent).toBe('sorted=0') + r.unmount() + }) + + it('throws a helpful error when a table component is used outside AppTable', () => { + expect(() => mount(OrphanTableConsumer, {})).toThrow( + /must be used within an `AppTable`/, + ) + }) +}) diff --git a/packages/octane-table/tests/conformance/core.test.ts b/packages/octane-table/tests/conformance/core.test.ts new file mode 100644 index 0000000000..f9e0116115 --- /dev/null +++ b/packages/octane-table/tests/conformance/core.test.ts @@ -0,0 +1,189 @@ +/** + * @tanstack/octane-table core conformance — `useTable`'s state wiring through + * octane's render path, against the REAL @tanstack/table-core. + * Ports the behaviors of upstream react-table's tests/core/core.test.tsx + * (markup render, stable api, rowModel) and adds the state-wiring matrix the + * upstream suite doesn't cover. + */ +import { describe, it, expect, beforeEach } from 'vitest' +import { mount, nextPaint } from '../_helpers' +import { + BasicTable, + SwapApp, + SortingTable, + ControlledSortingTable, + SelectorTable, + renders, + captured, + defaultData, + altData, +} from '../_fixtures/table-basic.tsrx' + +async function flush() { + for (let i = 0; i < 4; i++) { + await new Promise((r) => setTimeout(r, 0)) + await nextPaint() + } +} + +beforeEach(() => { + renders.basic = 0 + renders.sorting = 0 + renders.controlled = 0 + renders.selected = 0 + captured.table = undefined + captured.tables.length = 0 + captured.sortingStates.length = 0 + captured.selectedStates.length = 0 +}) + +describe('core (ports of upstream core.test.tsx)', () => { + it('renders a table with markup (thead/tbody/tfoot via flexRender)', async () => { + // Per react-table tests/core/core.test.tsx "renders a table with markup". + const r = mount(BasicTable, {}) + await flush() + const headers = r.findAll('thead th') + expect(headers.map((h) => h.textContent)).toEqual([ + 'First Name', + 'Last Name', + 'Age', + ]) + expect(r.findAll('tbody tr').length).toBe(3) + const firstRow = r.findAll('tbody tr')[0] + expect( + Array.from(firstRow.querySelectorAll('td')).map((c) => c.textContent), + ).toEqual(['tanner', 'linsley', '29']) + expect(r.findAll('tfoot th')[0].textContent).toBe('fn-footer') + r.unmount() + }) + + it('keeps one underlying table instance across re-renders', async () => { + // Per react-table tests/core/core.test.tsx "has a stable api", adapted to + // v9: `useTable` returns a FRESH wrapper object each render (it re-binds + // `state` and `options` to the values read during that render), so the + // stable thing is the underlying instance — its store and its state atoms, + // which is what actually must survive for state to persist. + const r = mount(SwapApp, {}) + await flush() + expect(captured.tables.length).toBeGreaterThan(0) + const first = captured.tables[0] as any + + r.click('#bump') // unrelated parent state + await flush() + expect(captured.tables.length).toBeGreaterThan(1) + for (const t of captured.tables as Array) { + expect(t.store).toBe(first.store) + expect(t.baseAtoms).toBe(first.baseAtoms) + } + r.unmount() + }) + + it('can return the rowModel', async () => { + // Per react-table tests/core/core.test.tsx "can return the rowModel". + const r = mount(BasicTable, {}) + await flush() + const table = captured.table as any + const model = table.getRowModel() + expect(model.rows.length).toBe(3) + expect(model.flatRows.length).toBe(3) + expect(model.rowsById['0'].original).toBe(defaultData[0]) + r.unmount() + }) + + it('propagates a data swap through the render-phase setOptions', async () => { + const r = mount(SwapApp, {}) + await flush() + + r.click('#swap-data') + await flush() + expect(r.findAll('tbody tr').length).toBe(2) + expect(r.findAll('tbody td')[0].textContent).toBe('kevin') + expect((captured.table as any).getRowModel().rows[0].original).toBe( + altData[0], + ) + + r.click('#swap-data') + await flush() + expect(r.findAll('tbody tr').length).toBe(3) + r.unmount() + }) + + it('propagates a columns swap', async () => { + const r = mount(SwapApp, {}) + await flush() + + r.click('#swap-cols') + await flush() + const headers = r.findAll('thead th') + expect(headers.map((h) => h.textContent)).toEqual(['Only Age']) + expect(r.findAll('tbody tr')[0].querySelectorAll('td').length).toBe(1) + r.unmount() + }) +}) + +describe('state wiring', () => { + const firstNames = (r: ReturnType) => + r.findAll('tbody tr').map((tr) => tr.querySelector('td')!.textContent) + + it('uncontrolled sorting toggles asc → desc → cleared', async () => { + const r = mount(SortingTable, {}) + await flush() + expect(firstNames(r)).toEqual(['tanner', 'derek', 'joe']) // natural order + + r.click('#s-th-firstName') + await flush() + expect(firstNames(r)).toEqual(['derek', 'joe', 'tanner']) // asc + expect(r.find('#s-th-firstName').textContent).toBe('First Name A') + + r.click('#s-th-firstName') + await flush() + expect(firstNames(r)).toEqual(['tanner', 'joe', 'derek']) // desc + expect(r.find('#s-th-firstName').textContent).toBe('First Name D') + + r.click('#s-th-firstName') + await flush() + expect(firstNames(r)).toEqual(['tanner', 'derek', 'joe']) // cleared (sortRemoval) + expect(r.find('#s-th-firstName').textContent).toBe('First Name') + r.unmount() + }) + + it('partially-controlled sorting flows through the parent state', async () => { + const r = mount(ControlledSortingTable, {}) + await flush() + + r.click('#c-th-firstName') + await flush() + const last = captured.sortingStates[captured.sortingStates.length - 1] + expect(last).toEqual([{ id: 'firstName', desc: false }]) + expect(r.findAll('.c-cell')[0].textContent).toBe('derek') // DOM reordered too + r.unmount() + }) + + it('exposes only the selected projection on table.state', async () => { + // v9's `useTable` takes a selector as its 2nd argument; `table.state` is + // that projection, not the full TableState. Re-renders are driven by the + // selected value (shallow-compared), so sorting must move it. + const r = mount(SelectorTable, {}) + await flush() + expect(r.find('#sel-state').textContent).toBe('n=0') + expect(Object.keys(captured.selectedStates[0] as object)).toEqual([ + 'sortCount', + ]) + + r.click('#sel-sort') + await flush() + expect(r.find('#sel-state').textContent).toBe('n=1') + r.unmount() + }) + + it('one state update re-renders exactly once (render-phase setOptions is loop-free)', async () => { + const r = mount(SortingTable, {}) + await flush() + const base = renders.sorting + + r.click('#s-th-firstName') + await flush() + expect(renders.sorting).toBe(base + 1) + r.unmount() + }) +}) diff --git a/packages/octane-table/tests/conformance/features.test.ts b/packages/octane-table/tests/conformance/features.test.ts new file mode 100644 index 0000000000..57963ed52d --- /dev/null +++ b/packages/octane-table/tests/conformance/features.test.ts @@ -0,0 +1,243 @@ +/** + * Feature smokes — every table-core feature row model wired through the + * octane render path: filtering (column + global + faceting), pagination, + * row selection (incl. the enableRowSelection predicate, per upstream + * RowSelection.test.tsx), column visibility (per upstream Visibility.test.tsx, + * plus group-header colSpan), expanding (with keyed-row DOM identity), and + * grouping/aggregation. + */ +import { describe, it, expect, beforeEach } from 'vitest' +import { mount, nextPaint } from '../_helpers' +import { + FilterTable, + PaginationTable, + SelectionTable, + VisibilityTable, + ExpandTable, + GroupTable, + captured, +} from '../_fixtures/features.tsrx' + +async function flush() { + for (let i = 0; i < 4; i++) { + await new Promise((r) => setTimeout(r, 0)) + await nextPaint() + } +} + +function input(r: ReturnType, selector: string, value: string) { + const el = r.find(selector) as HTMLInputElement + const proto = Object.getPrototypeOf(el) + Object.getOwnPropertyDescriptor(proto, 'value')!.set!.call(el, value) + el.dispatchEvent(new Event('input', { bubbles: true })) +} + +beforeEach(() => { + captured.table = undefined +}) + +describe('filtering + faceting', () => { + it('column filter narrows and restores via onInput', async () => { + const r = mount(FilterTable, {}) + await flush() + expect(r.findAll('.f-row').length).toBe(5) + + input(r, '#flt', 'an') // tanner, ryan + await flush() + expect(r.findAll('.f-row').map((li) => li.textContent)).toEqual([ + 'tanner', + 'ryan', + ]) + + input(r, '#flt', '') + await flush() + expect(r.findAll('.f-row').length).toBe(5) + r.unmount() + }) + + it('global filter matches across columns', async () => { + const r = mount(FilterTable, { global: true }) + await flush() + + input(r, '#gflt', 'complicated') + await flush() + expect(r.findAll('.f-row').map((li) => li.textContent)).toEqual([ + 'derek', + 'ryan', + ]) + r.unmount() + }) + + it('faceted unique values reflect the column data', async () => { + const r = mount(FilterTable, {}) + await flush() + const facets = captured.table.getColumn('status')!.getFacetedUniqueValues() + expect(facets.get('single')).toBe(3) + expect(facets.get('complicated')).toBe(2) + r.unmount() + }) +}) + +describe('pagination', () => { + it('windows rows, pages with buttons, and tracks canPrev/canNext', async () => { + const r = mount(PaginationTable, {}) + await flush() + expect(r.find('#pi').textContent).toBe('0/3') // 25 rows / pageSize 10 + expect(r.findAll('.p-row').length).toBe(10) + expect(r.findAll('.p-row')[0].textContent).toBe('p00') + expect((r.find('#prev') as HTMLButtonElement).disabled).toBe(true) + + r.click('#next') + await flush() + expect(r.find('#pi').textContent).toBe('1/3') + expect(r.findAll('.p-row')[0].textContent).toBe('p10') + expect((r.find('#prev') as HTMLButtonElement).disabled).toBe(false) + + r.click('#next') + await flush() + expect(r.find('#pi').textContent).toBe('2/3') + expect(r.findAll('.p-row').length).toBe(5) // last partial page + expect((r.find('#next') as HTMLButtonElement).disabled).toBe(true) + + r.click('#prev') + await flush() + expect(r.find('#pi').textContent).toBe('1/3') + r.unmount() + }) + + it('setPageSize re-windows and updates the page count', async () => { + const r = mount(PaginationTable, {}) + await flush() + + r.click('#ps-5') + await flush() + expect(r.find('#pi').textContent).toBe('0/5') + expect(r.findAll('.p-row').length).toBe(5) + r.unmount() + }) +}) + +describe('row selection (ports of upstream RowSelection.test.tsx behaviors)', () => { + it('selects and clears a single row', async () => { + const r = mount(SelectionTable, {}) + await flush() + expect(r.find('#sel-n').textContent).toBe('0') + + r.click('#cb-1') + await flush() + expect(r.find('#sel-n').textContent).toBe('1') + expect(captured.table.getRow('1').getIsSelected()).toBe(true) + expect(r.findAll('.sel-row')[1].getAttribute('data-selected')).toBe('1') + + r.click('#cb-1') + await flush() + expect(r.find('#sel-n').textContent).toBe('0') + expect(captured.table.getRow('1').getIsSelected()).toBe(false) + r.unmount() + }) + + it('select-all only selects rows the enableRowSelection predicate allows', async () => { + // Per upstream "Select all do not select rows which are not available". + const r = mount(SelectionTable, { predicated: true }) + await flush() + + r.click('#cb-all') + await flush() + // Only derek (45) and joe (42) pass age > 40. + expect(r.find('#sel-n').textContent).toBe('2') + expect( + r.findAll('.sel-row').map((li) => li.getAttribute('data-selected')), + ).toEqual(['0', '1', '1', '0', '0']) + // table-core counts only SELECTABLE rows for "all" (verified against the + // real core): every predicate-eligible row is selected → isAll true, and + // 2 of 5 flat rows → isSome (indeterminate) true. + expect(captured.table.getIsAllRowsSelected()).toBe(true) + expect(captured.table.getIsSomeRowsSelected()).toBe(true) + + r.click('#cb-all') + await flush() + expect(r.find('#sel-n').textContent).toBe('0') + expect(captured.table.getIsSomeRowsSelected()).toBe(false) + r.unmount() + }) + + it('ineligible rows render disabled checkboxes', async () => { + const r = mount(SelectionTable, { predicated: true }) + await flush() + expect((r.find('#cb-0') as HTMLInputElement).disabled).toBe(true) // tanner, 29 + expect((r.find('#cb-1') as HTMLInputElement).disabled).toBe(false) // derek, 45 + r.unmount() + }) +}) + +describe('column visibility (ports upstream Visibility.test.tsx behavior)', () => { + it('toggling a leaf column shrinks the group header colSpan and the row cells', async () => { + const r = mount(VisibilityTable, {}) + await flush() + // The group header is the first th of the FIRST header row (grouped + // header ids are depth-mangled — `1_name_firstName` — so query + // structurally, not by id). + const groupTh = () => + r.findAll('thead tr')[0].querySelector('th') as HTMLTableCellElement + expect(groupTh().textContent).toBe('Name') + expect(groupTh().colSpan).toBe(2) + expect(r.findAll('.v-row')[0].querySelectorAll('td').length).toBe(3) + + r.click('#vis-firstName') + await flush() + expect(groupTh().colSpan).toBe(1) + expect(r.findAll('.v-row')[0].querySelectorAll('td').length).toBe(2) + + r.click('#vis-firstName') + await flush() + expect(groupTh().colSpan).toBe(2) + expect(r.findAll('.v-row')[0].querySelectorAll('td').length).toBe(3) + r.unmount() + }) +}) + +describe('expanding', () => { + it('expands and collapses nested subRows, keeping parent row DOM identity', async () => { + const r = mount(ExpandTable, {}) + await flush() + expect(r.findAll('.e-row').length).toBe(2) + const rootA = r.find('[data-rid="0"]') + + r.click('[data-for="0"]') + await flush() + expect( + r.findAll('.e-row').map((li) => li.getAttribute('data-rid')), + ).toEqual(['0', '0.0', '0.1', '1']) + expect(r.find('[data-rid="0.1"]').getAttribute('data-depth')).toBe('1') + // Keyed @for reuses the parent row's DOM node across the expand. + expect(r.find('[data-rid="0"]')).toBe(rootA) + + r.click('[data-for="0.1"]') // nested expand + await flush() + expect(r.findAll('.e-row').length).toBe(5) + expect(r.find('[data-rid="0.1.0"]').getAttribute('data-depth')).toBe('2') + + r.click('[data-for="0"]') // collapse parent (child expansion retained in state) + await flush() + expect(r.findAll('.e-row').length).toBe(2) + r.unmount() + }) +}) + +describe('grouping + aggregation', () => { + it('renders grouped rows with aggregated cells through flexRender', async () => { + const r = mount(GroupTable, {}) + await flush() + const grouped = r.findAll('.g-row[data-grouped="1"]') + expect(grouped.length).toBe(2) // single, complicated + // Aggregated age sums: single = 29+42+27, complicated = 45+38. + const texts = grouped.map((li) => li.textContent) + expect(texts[0]).toContain('single') + expect(texts[0]).toContain('sum:98') + expect(texts[1]).toContain('complicated') + expect(texts[1]).toContain('sum:83') + // Leaf rows render underneath (expanded: true). + expect(r.findAll('.g-row').length).toBe(7) // 2 group rows + 5 leaves + r.unmount() + }) +}) diff --git a/packages/octane-table/tests/conformance/flex-render.test.ts b/packages/octane-table/tests/conformance/flex-render.test.ts new file mode 100644 index 0000000000..b3aa654fb5 --- /dev/null +++ b/packages/octane-table/tests/conformance/flex-render.test.ts @@ -0,0 +1,94 @@ +/** + * flexRender conformance — every renderer shape a columnDef can carry, through + * octane's value-position rendering: string, render-fn, component (props + * arrive), pre-created descriptor passthrough, undefined, octane memo() + * (a plain function — the branch upstream needed exotic-component sniffing + * for), and the upstream falsy quirk pinned as-is. + */ +import { describe, it, expect, beforeEach } from 'vitest' +import { flexRender } from '@tanstack/octane-table' +import { mount, nextPaint } from '../_helpers' +import { FlexTable, cellProps } from '../_fixtures/flex-render.tsrx' + +async function flush() { + for (let i = 0; i < 4; i++) { + await new Promise((r) => setTimeout(r, 0)) + await nextPaint() + } +} + +beforeEach(() => { + cellProps.length = 0 +}) + +describe('flexRender through the octane render path', () => { + it('renders a string header as text', async () => { + const r = mount(FlexTable, {}) + await flush() + expect(r.find('#f-th-firstName').textContent).toBe('First Name') + r.unmount() + }) + + it('renders a render-fn header with its context', async () => { + const r = mount(FlexTable, {}) + await flush() + expect(r.find('#f-th-lastName').textContent).toBe('hdr:lastName') + r.unmount() + }) + + it('renders a component cell and passes the full cell context as props', async () => { + const r = mount(FlexTable, {}) + await flush() + const cells = r.findAll('.name-cell') + expect(cells.map((c) => c.textContent)).toEqual(['nc:tanner', 'nc:derek']) + expect(cellProps.length).toBeGreaterThan(0) + const props = cellProps[0] as Record + for (const key of [ + 'getValue', + 'renderValue', + 'row', + 'column', + 'table', + 'cell', + ]) { + expect( + props, + `cell component missing context prop ${key}`, + ).toHaveProperty(key) + } + r.unmount() + }) + + it('renders nothing (no crash) for an undefined renderer', async () => { + const r = mount(FlexTable, {}) + await flush() + expect(r.find('#f-th-age').textContent).toBe('') + r.unmount() + }) + + it('passes a pre-created element descriptor through the non-component branch', async () => { + const r = mount(FlexTable, {}) + await flush() + const statics = r.findAll('.static-cell') + expect(statics.length).toBe(2) // one per row, same descriptor value + expect(statics[0].textContent).toBe('static') + r.unmount() + }) + + it('renders an octane memo()-wrapped component cell (plain function, no exotic sniffing)', async () => { + const r = mount(FlexTable, {}) + await flush() + const cells = r.findAll('.age-cell') + expect(cells.map((c) => c.textContent)).toEqual(['mc:29', 'mc:40']) + r.unmount() + }) + + it('treats only null and undefined as empty renderers', () => { + expect(flexRender(undefined, {})).toBe(null) + expect(flexRender(null, {})).toBe(null) + expect(flexRender('' as never, {})).toBe('') + expect(flexRender(0 as never, {})).toBe(0) + expect(flexRender('x', {})).toBe('x') + expect(flexRender(42 as never, {})).toBe(42) + }) +}) diff --git a/packages/octane-table/tests/conformance/parity.test.ts b/packages/octane-table/tests/conformance/parity.test.ts new file mode 100644 index 0000000000..a3169446be --- /dev/null +++ b/packages/octane-table/tests/conformance/parity.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest' +import * as core from '@tanstack/table-core' +import * as binding from '@tanstack/octane-table' + +describe('export surface', () => { + it('re-exports Table core and the Octane adapter APIs without legacy APIs', () => { + const adapterExports = [ + 'FlexRender', + 'Subscribe', + 'createTableHook', + 'createTableHookContexts', + 'flexRender', + 'useTable', + ] + const expected = [...Object.keys(core), ...adapterExports].sort() + + expect(Object.keys(binding).sort()).toEqual(expected) + expect(binding).not.toHaveProperty('useLegacyTable') + }) + + it('re-exports the same @tanstack/table-core module instance', () => { + expect(binding.createColumnHelper).toBe(core.createColumnHelper) + expect(binding.constructTable).toBe(core.constructTable) + expect(binding.tableFeatures).toBe(core.tableFeatures) + expect(binding.createSortedRowModel).toBe(core.createSortedRowModel) + expect(binding.sortFns).toBe(core.sortFns) + }) +}) diff --git a/packages/octane-table/tests/ssr/ssr.test.ts b/packages/octane-table/tests/ssr/ssr.test.ts new file mode 100644 index 0000000000..59b3cf04b3 --- /dev/null +++ b/packages/octane-table/tests/ssr/ssr.test.ts @@ -0,0 +1,19 @@ +import { renderToString } from 'octane/server' +import { describe, expect, it } from 'vitest' +import { SsrTable } from '../_fixtures/ssr-table.tsrx' + +describe('Octane table SSR', () => { + it('renders selected table state and FlexRender content without a DOM', () => { + expect(globalThis.document).toBeUndefined() + + const { html } = renderToString(SsrTable, {}) + const visibleHtml = html.replace(//g, '') + + expect(visibleHtml).toContain('id="ssr-state">2') + expect(visibleHtml).toContain( + 'id="ssr-direct">Person
', + ) + expect(visibleHtml).toContain('Ada') + expect(visibleHtml).toContain('37') + }) +}) diff --git a/packages/octane-table/tests/test-setup.ts b/packages/octane-table/tests/test-setup.ts new file mode 100644 index 0000000000..58d5e2e535 --- /dev/null +++ b/packages/octane-table/tests/test-setup.ts @@ -0,0 +1,7 @@ +import { afterEach } from 'vitest' +import { drainPassiveEffects } from 'octane' + +afterEach(() => { + drainPassiveEffects() + document.body.replaceChildren() +}) diff --git a/packages/octane-table/tests/type-tests/public-surface.tsx b/packages/octane-table/tests/type-tests/public-surface.tsx new file mode 100644 index 0000000000..cd148a6c1e --- /dev/null +++ b/packages/octane-table/tests/type-tests/public-surface.tsx @@ -0,0 +1,100 @@ +/** @jsxImportSource octane */ +import { createAtom } from '@tanstack/octane-store' +import { + Subscribe, + createTableHook, + createTableHookContexts, + rowSelectionFeature, + tableFeatures, + useTable, +} from '@tanstack/octane-table' +import { Subscribe as SidecarSubscribe } from '../../src/Subscribe.tsrx' +import { createTableHook as sidecarCreateTableHook } from '../../src/createTableHook.tsrx' +import { useTable as sidecarUseTable } from '../../src/useTable.tsrx' +import type { + ColumnDef, + RowSelectionState, + TableState, +} from '@tanstack/octane-table' + +// @ts-expect-error Octane intentionally has no legacy adapter subpath. +type LegacyAdapter = typeof import('@tanstack/octane-table/legacy') + +type Row = { id: string; label: string } +const features = tableFeatures({ rowSelectionFeature }) +const columns: Array> = [ + { accessorKey: 'label' }, +] +const externalSelection = createAtom({}) + +function RegisteredCell({ prefix }: { prefix: string }) { + return {prefix} +} + +const contexts = createTableHookContexts() +const app = createTableHook({ + features, + ...contexts, + cellComponents: { RegisteredCell }, +}) + +const helper = app.createAppColumnHelper() +helper.accessor('label', { + cell: ({ cell }) => , +}) + +export function PublicTypeFixture() { + const table = useTable( + { + features, + columns, + data: [{ id: '1', label: 'One' }], + atoms: { rowSelection: externalSelection }, + }, + (state) => ({ + selected: state.rowSelection, + }), + ) + + const selected: RowSelectionState = table.state.selected + const fullState: TableState = table.store.get() + void selected + void fullState + + return ( +
+ { + const typed: RowSelectionState = selection + return {String(Object.keys(typed).length)} + }} + /> + Boolean(selection['1'])} + children={(selectedValue) => { + const typed: boolean = selectedValue + return {String(typed)} + }} + /> +
+ ) +} + +const invalidSubscribe = ( + // @ts-expect-error Identity subscriptions pass RowSelectionState to children. + {String(value)}} + /> +) + +const checkedUseTable: typeof useTable = sidecarUseTable +const checkedSubscribe: typeof Subscribe = SidecarSubscribe +const checkedFactory: typeof createTableHook = sidecarCreateTableHook +void checkedUseTable +void checkedSubscribe +void checkedFactory +void invalidSubscribe +void (null as LegacyAdapter | null) diff --git a/packages/octane-table/tsconfig.docs.json b/packages/octane-table/tsconfig.docs.json new file mode 100644 index 0000000000..fce002d907 --- /dev/null +++ b/packages/octane-table/tsconfig.docs.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["docs-entry.d.ts", "src"] +} diff --git a/packages/octane-table/tsconfig.json b/packages/octane-table/tsconfig.json new file mode 100644 index 0000000000..961c4d8632 --- /dev/null +++ b/packages/octane-table/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "strict": true, + "noEmit": true, + "noErrorTruncation": true, + "resolveJsonModule": true, + "verbatimModuleSyntax": true, + "jsx": "react-jsx", + "jsxImportSource": "octane", + "allowImportingTsExtensions": true, + "types": ["node", "vitest/globals"] + }, + "include": ["src", "tests"] +} diff --git a/packages/octane-table/vite.config.ts b/packages/octane-table/vite.config.ts new file mode 100644 index 0000000000..5e325ad233 --- /dev/null +++ b/packages/octane-table/vite.config.ts @@ -0,0 +1,23 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import { octane } from 'octane/compiler/vite' +import packageJson from './package.json' + +export default defineConfig({ + plugins: [octane({ ssr: false })], + resolve: { + alias: { + [packageJson.name]: fileURLToPath( + new URL('./src/index.ts', import.meta.url), + ), + }, + }, + test: { + name: packageJson.name, + dir: './tests', + exclude: ['**/ssr/**/*.test.ts'], + watch: false, + environment: 'jsdom', + setupFiles: ['./tests/test-setup.ts'], + }, +}) diff --git a/packages/octane-table/vitest.ssr.config.ts b/packages/octane-table/vitest.ssr.config.ts new file mode 100644 index 0000000000..671947266b --- /dev/null +++ b/packages/octane-table/vitest.ssr.config.ts @@ -0,0 +1,21 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import { octane } from 'octane/compiler/vite' +import packageJson from './package.json' + +export default defineConfig({ + plugins: [octane({ ssr: true })], + resolve: { + alias: { + [packageJson.name]: fileURLToPath( + new URL('./src/index.ts', import.meta.url), + ), + }, + }, + test: { + name: `${packageJson.name}:ssr`, + include: ['tests/ssr/**/*.test.ts'], + watch: false, + environment: 'node', + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a96e56b0e5..c39bbdfcc6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,7 @@ overrides: '@tanstack/ember-table': workspace:* '@tanstack/lit-table': workspace:* '@tanstack/match-sorter-utils': workspace:* + '@tanstack/octane-table': workspace:* '@tanstack/preact-table-devtools': workspace:* '@tanstack/preact-table': workspace:* '@tanstack/react-table-devtools': workspace:* @@ -68,7 +69,7 @@ importers: version: 10.5.0(jiti@2.7.0) jsdom: specifier: ^29.1.1 - version: 29.1.1 + version: 29.1.1(@noble/hashes@2.2.0) knip: specifier: ^6.16.1 version: 6.16.1 @@ -113,7 +114,7 @@ importers: version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) vitest: specifier: ^4.1.9 - version: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + version: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) examples/alpine/aggregation: dependencies: @@ -5898,6 +5899,712 @@ importers: specifier: ^8.1.0 version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + examples/octane/aggregation: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-store': + specifier: ^0.12.0 + version: 0.12.0(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0))) + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/basic-dynamic-columns: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/basic-external-atoms: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-store': + specifier: ^0.12.0 + version: 0.12.0(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0))) + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/basic-external-state: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/basic-subscribe: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-store': + specifier: ^0.12.0 + version: 0.12.0(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0))) + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/basic-use-app-table: + dependencies: + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/basic-use-table: + dependencies: + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/column-ordering: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/column-pinning: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/column-pinning-split: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/column-pinning-sticky: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/column-resizing: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/column-resizing-performant: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/column-sizing: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/column-visibility: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/composable-tables: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-store': + specifier: ^0.12.0 + version: 0.12.0(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0))) + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/custom-plugin: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/expanding: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/filters: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/match-sorter-utils': + specifier: workspace:* + version: link:../../../packages/match-sorter-utils + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/filters-faceted: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/match-sorter-utils': + specifier: workspace:* + version: link:../../../packages/match-sorter-utils + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/filters-faceted-bucketed: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/filters-fuzzy: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/match-sorter-utils': + specifier: workspace:* + version: link:../../../packages/match-sorter-utils + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/grouped-aggregation: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/grouping: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/header-groups: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/kitchen-sink: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/match-sorter-utils': + specifier: workspace:* + version: link:../../../packages/match-sorter-utils + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/pagination: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-store': + specifier: ^0.12.0 + version: 0.12.0(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0))) + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/row-pinning: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/row-selection: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-store': + specifier: ^0.12.0 + version: 0.12.0(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0))) + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/sorting: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + + examples/octane/sub-components: + dependencies: + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 + '@tanstack/octane-table': + specifier: workspace:* + version: link:../../../packages/octane-table + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + examples/preact/aggregation: dependencies: '@faker-js/faker': @@ -13453,6 +14160,25 @@ importers: specifier: 0.5.0 version: 0.5.0 + packages/octane-table: + dependencies: + '@tanstack/octane-store': + specifier: ^0.12.0 + version: 0.12.0(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0))) + '@tanstack/table-core': + specifier: workspace:* + version: link:../table-core + devDependencies: + '@tsrx/typescript-plugin': + specifier: ^0.3.118 + version: 0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3) + octane: + specifier: 0.1.21 + version: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + typescript: + specifier: 6.0.3 + version: 6.0.3 + packages/preact-table: dependencies: '@tanstack/preact-store': @@ -13626,7 +14352,7 @@ importers: version: 7.1.2(svelte@5.56.2(@typescript-eslint/types@8.62.0))(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0)) '@testing-library/svelte': specifier: ^5.4.2 - version: 5.4.2(svelte@5.56.2(@typescript-eslint/types@8.62.0)) + version: 5.4.2(svelte@5.56.2(@typescript-eslint/types@8.62.0))(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0))(vitest@4.1.9) eslint-plugin-svelte: specifier: ^3.19.0 version: 3.19.0(eslint@10.5.0(jiti@2.7.0))(svelte@5.56.2(@typescript-eslint/types@8.62.0)) @@ -16255,6 +16981,10 @@ packages: resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} + '@noble/hashes@2.2.0': + resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -16362,6 +17092,9 @@ packages: cpu: [x64] os: [win32] + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@one-ini/wasm@0.2.1': resolution: {integrity: sha512-TUqERXGNTifZ9y2g3wPxQrw3HpHv/02DsW3D90T9x0hhonrL1ZqpSmNrU2XkoIq0fP1N6gZfVQzy2Fw1ZvGBNg==} @@ -18912,8 +19645,8 @@ packages: peerDependencies: eslint: ^9.0.0 || ^10.0.0 - '@sveltejs/acorn-typescript@1.0.10': - resolution: {integrity: sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==} + '@sveltejs/acorn-typescript@1.0.11': + resolution: {integrity: sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==} peerDependencies: acorn: ^8.9.0 @@ -19231,6 +19964,12 @@ packages: peerDependencies: lit: ^3.1.0 + '@tanstack/octane-store@0.12.0': + resolution: {integrity: sha512-6/iR8gjk0qcFxbuxGeJSSqBRlelEanDm15853sCNXYbJ2JjWO6XJfWUW7/WNrfYu2lTR3nC8R6Zpi/VdB9HZZA==} + engines: {node: '>=22'} + peerDependencies: + octane: 0.1.21 + '@tanstack/pacer-lite@0.1.1': resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} engines: {node: '>=18'} @@ -19628,7 +20367,7 @@ packages: engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 - '@types/react': ^18.0.0 || ^19.0.0 + '@types/react': 19.2.16 '@types/react-dom': ^18.0.0 || ^19.0.0 react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 @@ -19673,6 +20412,34 @@ packages: '@tsconfig/svelte@5.0.8': resolution: {integrity: sha512-UkNnw1/oFEfecR8ypyHIQuWYdkPvHiwcQ78sh+ymIiYoF+uc5H1UBetbjyqT+vgGJ3qQN6nhucJviX6HesWtKQ==} + '@tsrx/core@0.1.56': + resolution: {integrity: sha512-SZ+lani1n0yR4pFqOxclfWvU4Pvd3i9d6aptShicAzC3IYW2yQrsNJoG5ZqoVai9A3ZRikkO4CY8/HK+ksNp1Q==} + + '@tsrx/typescript-plugin@0.3.118': + resolution: {integrity: sha512-70yYCbX7v2pyrQcdSoQdvDFe0WbpnjjvVdqzc5weMXKEit4x617Qj2z61bVzjPX+6QgMPqG4cioW5/lMKG84mw==} + hasBin: true + peerDependencies: + '@tsrx/preact': 0.1.56 + '@tsrx/react': 0.2.56 + '@tsrx/ripple': 0.1.57 + '@tsrx/solid': 0.1.56 + '@tsrx/vue': 0.1.56 + octane: '*' + typescript: ^5.9.3 + peerDependenciesMeta: + '@tsrx/preact': + optional: true + '@tsrx/react': + optional: true + '@tsrx/ripple': + optional: true + '@tsrx/solid': + optional: true + '@tsrx/vue': + optional: true + octane: + optional: true + '@tufjs/canonical-json@2.0.0': resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==} engines: {node: ^16.14.0 || >=18.0.0} @@ -19693,6 +20460,9 @@ packages: '@types/alpinejs@3.13.11': resolution: {integrity: sha512-3KhGkDixCPiLdL3Z/ok1GxHwLxEWqQOKJccgaQL01wc0EVM2tCTaqlC3NIedmxAXkVzt/V6VTM8qPgnOHKJ1MA==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -19735,6 +20505,9 @@ packages: '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -20521,6 +21294,10 @@ packages: resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==} hasBin: true + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + abbrev@4.0.0: resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} engines: {node: ^20.17.0 || >=22.9.0} @@ -20549,6 +21326,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + adjust-sourcemap-loader@4.0.0: resolution: {integrity: sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==} engines: {node: '>=8.9'} @@ -20635,6 +21417,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -21176,6 +21962,10 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -21623,6 +22413,10 @@ packages: resolution: {integrity: sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==} engines: {node: '>=4'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -21637,8 +22431,8 @@ packages: detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - devalue@5.8.1: - resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + devalue@5.8.2: + resolution: {integrity: sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==} diff@8.0.4: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} @@ -21648,6 +22442,9 @@ packages: resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} engines: {node: '>=6'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} @@ -22170,8 +22967,8 @@ packages: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} - esrap@2.2.11: - resolution: {integrity: sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ==} + esrap@2.3.0: + resolution: {integrity: sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==} peerDependencies: '@typescript-eslint/types': ^8.2.0 peerDependenciesMeta: @@ -22835,6 +23632,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ini@6.0.0: resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -23160,6 +23960,9 @@ packages: engines: {node: '>=14'} hasBin: true + js-cookie@3.0.8: + resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} + js-string-escape@1.0.1: resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} engines: {node: '>= 0.8'} @@ -23499,6 +24302,10 @@ packages: lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -23952,6 +24759,21 @@ packages: resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} engines: {node: '>=12.20.0'} + octane@0.1.21: + resolution: {integrity: sha512-cfemarQcdslN3WG9f/e1EzcwO2JvbJQLliLp3fF6ECiel28vT5ncaigSbgGQZgMWYX/1CWM5ChlB5xoqOVn72Q==} + engines: {node: '>=22'} + peerDependencies: + react: ^19.0.0 + react-dom: ^19.0.0 + vite: ^8.0.16 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + vite: + optional: true + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -24412,6 +25234,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -24557,6 +25382,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@19.2.7: resolution: {integrity: sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==} @@ -26121,6 +26949,9 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue-component-type-helpers@3.3.8: + resolution: {integrity: sha512-troqCMmQodQDqUqn63NQaFi+CDSclSe7sc8VEBFqf5GFLqmGR2Ph3P2WEC7qwpRVyEWsTi/aAr4vyOe/B1hU3g==} + vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} @@ -26470,78 +27301,8 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} - '@one-ini/wasm@0.1.1': - resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} - - '@types/aria-query@5.0.4': - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - - abbrev@2.0.0: - resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - - commander@10.0.1: - resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} - engines: {node: '>=14'} - - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - - dom-accessibility-api@0.5.16: - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - - js-cookie@3.0.8: - resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} - - lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - - proto-list@1.2.4: - resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - - vue-component-type-helpers@3.3.8: - resolution: {integrity: sha512-troqCMmQodQDqUqn63NQaFi+CDSclSe7sc8VEBFqf5GFLqmGR2Ph3P2WEC7qwpRVyEWsTi/aAr4vyOe/B1hU3g==} - snapshots: - '@one-ini/wasm@0.1.1': {} - - '@types/aria-query@5.0.4': {} - - abbrev@2.0.0: {} - - ansi-styles@5.2.0: {} - - commander@10.0.1: {} - - dequal@2.0.3: {} - - dom-accessibility-api@0.5.16: {} - - ini@1.3.8: {} - - js-cookie@3.0.8: {} - - lz-string@1.5.0: {} - - proto-list@1.2.4: {} - - react-is@17.0.2: {} - - vue-component-type-helpers@3.3.8: {} - '@adobe/css-tools@4.5.0': {} '@adobe/react-spectrum-ui@1.2.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': @@ -26698,7 +27459,7 @@ snapshots: '@analogjs/vite-plugin-angular': 2.6.1(@angular-devkit/build-angular@22.0.0(76c7c1e35ac868abb4049c35b81db047))(@angular/build@22.0.0(64f0fa42bd8e1e97164699f0eb2e86f9))(@angular/compiler@22.0.2)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) '@angular-devkit/architect': 0.2200.0(chokidar@5.0.0) '@angular-devkit/schematics': 22.0.0(chokidar@5.0.0) - vitest: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + vitest: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) '@angular-devkit/architect@0.2200.0(chokidar@5.0.0)': dependencies: @@ -26985,7 +27746,7 @@ snapshots: ng-packagr: 22.0.0(@angular/compiler-cli@22.0.2(@angular/compiler@22.0.2)(typescript@6.0.3))(tailwindcss@4.3.1)(tslib@2.8.1)(typescript@6.0.3) postcss: 8.5.13 tailwindcss: 4.3.1 - vitest: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + vitest: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) transitivePeerDependencies: - '@types/node' - chokidar @@ -27042,7 +27803,7 @@ snapshots: ng-packagr: 22.0.0(@angular/compiler-cli@22.0.2(@angular/compiler@22.0.2)(typescript@6.0.3))(tailwindcss@4.3.1)(tslib@2.8.1)(typescript@6.0.3) postcss: 8.5.16 tailwindcss: 4.3.1 - vitest: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@7.3.2(@types/node@26.0.0)(jiti@2.7.0)(less@4.6.4)(lightningcss@1.32.0)(sass@1.99.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + vitest: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@7.3.2(@types/node@26.0.0)(jiti@2.7.0)(less@4.6.4)(lightningcss@1.32.0)(sass@1.99.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) transitivePeerDependencies: - '@types/node' - chokidar @@ -27098,7 +27859,7 @@ snapshots: ng-packagr: 22.0.0(@angular/compiler-cli@22.0.2(@angular/compiler@22.0.2)(typescript@6.0.3))(tailwindcss@4.3.1)(tslib@2.8.1)(typescript@6.0.3) postcss: 8.5.15 tailwindcss: 4.3.1 - vitest: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + vitest: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) transitivePeerDependencies: - '@types/node' - chokidar @@ -27154,7 +27915,7 @@ snapshots: ng-packagr: 22.0.0(@angular/compiler-cli@22.0.2(@angular/compiler@22.0.2)(typescript@6.0.3))(tailwindcss@4.3.1)(tslib@2.8.1)(typescript@6.0.3) postcss: 8.5.13 tailwindcss: 4.3.1 - vitest: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.99.0)(sugarss@5.0.1(postcss@8.5.13))(terser@5.46.2)(yaml@2.9.0)) + vitest: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.99.0)(sugarss@5.0.1(postcss@8.5.13))(terser@5.46.2)(yaml@2.9.0)) transitivePeerDependencies: - '@types/node' - chokidar @@ -29472,7 +30233,9 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@exodus/bytes@1.15.1': {} + '@exodus/bytes@1.15.1(@noble/hashes@2.2.0)': + optionalDependencies: + '@noble/hashes': 2.2.0 '@faker-js/faker@10.5.0': {} @@ -30362,6 +31125,8 @@ snapshots: '@noble/hashes@1.4.0': {} + '@noble/hashes@2.2.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -30479,6 +31244,8 @@ snapshots: '@nx/nx-win32-x64-msvc@22.7.5': optional: true + '@one-ini/wasm@0.1.1': {} + '@one-ini/wasm@0.2.1': {} '@oozcitak/dom@2.0.2': @@ -32570,9 +33337,9 @@ snapshots: estraverse: 5.3.0 picomatch: 4.0.5 - '@sveltejs/acorn-typescript@1.0.10(acorn@8.16.0)': + '@sveltejs/acorn-typescript@1.0.11(acorn@8.18.0)': dependencies: - acorn: 8.16.0 + acorn: 8.18.0 '@sveltejs/load-config@0.1.1': {} @@ -32906,6 +33673,11 @@ snapshots: '@tanstack/virtual-core': 3.17.3 lit: 3.3.3 + '@tanstack/octane-store@0.12.0(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))': + dependencies: + '@tanstack/store': 0.11.0 + octane: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + '@tanstack/pacer-lite@0.1.1': {} '@tanstack/pacer@0.21.1': @@ -33450,11 +34222,14 @@ snapshots: dependencies: svelte: 5.56.2(@typescript-eslint/types@8.62.0) - '@testing-library/svelte@5.4.2(svelte@5.56.2(@typescript-eslint/types@8.62.0))': + '@testing-library/svelte@5.4.2(svelte@5.56.2(@typescript-eslint/types@8.62.0))(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0))(vitest@4.1.9)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/svelte-core': 1.1.3(svelte@5.56.2(@typescript-eslint/types@8.62.0)) svelte: 5.56.2(@typescript-eslint/types@8.62.0) + optionalDependencies: + vite: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + vitest: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0)) '@testing-library/vue@8.1.0(@vue/compiler-dom@3.5.38)(@vue/compiler-sfc@3.5.38)(@vue/server-renderer@3.5.38(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3))': dependencies: @@ -33477,6 +34252,29 @@ snapshots: '@tsconfig/svelte@5.0.8': {} + '@tsrx/core@0.1.56(@typescript-eslint/types@8.62.0)': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@noble/hashes': 2.2.0 + '@sveltejs/acorn-typescript': 1.0.11(acorn@8.18.0) + '@types/estree': 1.0.9 + '@types/estree-jsx': 1.0.5 + acorn: 8.18.0 + esrap: 2.3.0(@typescript-eslint/types@8.62.0) + is-reference: 3.0.3 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' + + '@tsrx/typescript-plugin@0.3.118(octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)))(typescript@6.0.3)': + dependencies: + '@volar/language-core': 2.4.28 + '@volar/typescript': 2.4.28 + typescript: 6.0.3 + optionalDependencies: + octane: 0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + '@tufjs/canonical-json@2.0.0': {} '@tufjs/models@4.1.0': @@ -33500,6 +34298,8 @@ snapshots: '@types/alpinejs@3.13.11': {} + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.7 @@ -33562,6 +34362,10 @@ snapshots: '@types/esrecurse@4.3.1': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + '@types/estree@1.0.8': {} '@types/estree@1.0.9': {} @@ -33963,7 +34767,7 @@ snapshots: obug: 2.1.3 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) + vitest: 4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) '@vitest/expect@4.1.9': dependencies: @@ -33983,6 +34787,15 @@ snapshots: vite: 7.3.2(@types/node@26.0.0)(jiti@2.7.0)(less@4.6.4)(lightningcss@1.32.0)(sass@1.99.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) optional: true + '@vitest/mocker@4.1.9(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + optional: true + '@vitest/mocker@4.1.9(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.9 @@ -34847,6 +35660,8 @@ snapshots: dependencies: argparse: 2.0.1 + abbrev@2.0.0: {} + abbrev@4.0.0: {} accepts@1.3.8: @@ -34859,16 +35674,22 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-import-phases@1.0.4(acorn@8.16.0): + acorn-import-phases@1.0.4(acorn@8.18.0): dependencies: - acorn: 8.16.0 + acorn: 8.18.0 acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + acorn@8.16.0: {} + acorn@8.18.0: {} + adjust-sourcemap-loader@4.0.0: dependencies: loader-utils: 2.0.4 @@ -34955,6 +35776,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} ansis@4.3.1: {} @@ -34986,7 +35809,6 @@ snapshots: dependencies: call-bound: 1.0.4 is-array-buffer: 3.0.5 - optional: true array-equal@1.0.2: {} @@ -35079,7 +35901,6 @@ snapshots: available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 - optional: true axios@1.16.0(debug@4.4.3): dependencies: @@ -35669,6 +36490,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + commander@10.0.1: {} + commander@14.0.3: {} commander@2.20.3: {} @@ -35883,10 +36706,10 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - data-urls@7.0.0: + data-urls@7.0.0(@noble/hashes@2.2.0): dependencies: whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 + whatwg-url: 16.0.1(@noble/hashes@2.2.0) transitivePeerDependencies: - '@noble/hashes' @@ -35988,7 +36811,6 @@ snapshots: define-data-property: 1.1.4 has-property-descriptors: 1.0.2 object-keys: 1.1.1 - optional: true defu@6.1.7: {} @@ -36000,6 +36822,8 @@ snapshots: dependency-graph@1.0.0: {} + dequal@2.0.3: {} + destroy@1.2.0: {} detect-libc@2.1.2: {} @@ -36008,7 +36832,7 @@ snapshots: detect-node@2.1.0: {} - devalue@5.8.1: {} + devalue@5.8.2: {} diff@8.0.4: {} @@ -36016,6 +36840,8 @@ snapshots: dependencies: '@leichtgewicht/ip-codec': 2.0.5 + dom-accessibility-api@0.5.16: {} + dom-accessibility-api@0.6.3: {} dom-element-descriptors@0.5.1: {} @@ -36401,7 +37227,6 @@ snapshots: es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 - optional: true es-set-tostringtag@2.1.0: dependencies: @@ -36836,8 +37661,8 @@ snapshots: espree@10.4.0: dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 4.2.1 espree@11.2.0: @@ -36852,7 +37677,7 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.2.11(@typescript-eslint/types@8.62.0): + esrap@2.3.0(@typescript-eslint/types@8.62.0): dependencies: '@jridgewell/sourcemap-codec': 1.5.5 optionalDependencies: @@ -37134,7 +37959,6 @@ snapshots: for-each@0.3.5: dependencies: is-callable: 1.2.7 - optional: true foreground-child@3.3.1: dependencies: @@ -37250,8 +38074,7 @@ snapshots: is-document.all: 1.0.0 optional: true - functions-have-names@1.2.3: - optional: true + functions-have-names@1.2.3: {} generator-function@2.0.1: optional: true @@ -37391,8 +38214,7 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - has-bigints@1.1.0: - optional: true + has-bigints@1.1.0: {} has-flag@3.0.0: {} @@ -37474,9 +38296,9 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 - html-encoding-sniffer@6.0.0: + html-encoding-sniffer@6.0.0(@noble/hashes@2.2.0): dependencies: - '@exodus/bytes': 1.15.1 + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) transitivePeerDependencies: - '@noble/hashes' @@ -37632,6 +38454,8 @@ snapshots: inherits@2.0.4: {} + ini@1.3.8: {} + ini@6.0.0: {} injection-js@2.6.1: @@ -37649,8 +38473,7 @@ snapshots: dependencies: es-errors: 1.3.0 hasown: 2.0.4 - side-channel: 1.1.0 - optional: true + side-channel: 1.1.1 intl-messageformat@10.7.18: dependencies: @@ -37675,7 +38498,6 @@ snapshots: call-bind: 1.0.9 call-bound: 1.0.4 get-intrinsic: 1.3.0 - optional: true is-arrayish@0.2.1: {} @@ -37691,7 +38513,6 @@ snapshots: is-bigint@1.1.0: dependencies: has-bigints: 1.1.0 - optional: true is-binary-path@2.1.0: dependencies: @@ -37701,10 +38522,8 @@ snapshots: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 - optional: true - is-callable@1.2.7: - optional: true + is-callable@1.2.7: {} is-core-module@2.16.2: dependencies: @@ -37721,7 +38540,6 @@ snapshots: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 - optional: true is-docker@2.2.1: {} @@ -37772,8 +38590,7 @@ snapshots: is-interactive@2.0.0: {} - is-map@2.0.3: - optional: true + is-map@2.0.3: {} is-negative-zero@2.0.3: optional: true @@ -37784,7 +38601,6 @@ snapshots: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 - optional: true is-number@7.0.0: {} @@ -37814,15 +38630,12 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 hasown: 2.0.4 - optional: true - is-set@2.0.3: - optional: true + is-set@2.0.3: {} is-shared-array-buffer@1.0.4: dependencies: call-bound: 1.0.4 - optional: true is-stream@2.0.1: {} @@ -37832,7 +38645,6 @@ snapshots: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 - optional: true is-subdir@1.2.0: dependencies: @@ -37843,7 +38655,6 @@ snapshots: call-bound: 1.0.4 has-symbols: 1.1.0 safe-regex-test: 1.1.0 - optional: true is-typed-array@1.1.15: dependencies: @@ -37854,8 +38665,7 @@ snapshots: is-unicode-supported@2.1.0: {} - is-weakmap@2.0.2: - optional: true + is-weakmap@2.0.2: {} is-weakref@1.1.1: dependencies: @@ -37866,7 +38676,6 @@ snapshots: dependencies: call-bound: 1.0.4 get-intrinsic: 1.3.0 - optional: true is-what@4.1.16: {} @@ -37949,6 +38758,8 @@ snapshots: js-cookie: 3.0.8 nopt: 7.2.1 + js-cookie@3.0.8: {} + js-string-escape@1.0.1: {} js-tokens@10.0.0: {} @@ -37987,17 +38798,17 @@ snapshots: - supports-color - utf-8-validate - jsdom@29.1.1: + jsdom@29.1.1(@noble/hashes@2.2.0): dependencies: '@asamuzakjp/css-color': 5.1.11 '@asamuzakjp/dom-selector': 7.1.1 '@bramus/specificity': 2.4.2 '@csstools/css-syntax-patches-for-csstree': 1.1.5(css-tree@3.2.1) - '@exodus/bytes': 1.15.1 + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) css-tree: 3.2.1 - data-urls: 7.0.0 + data-urls: 7.0.0(@noble/hashes@2.2.0) decimal.js: 10.6.0 - html-encoding-sniffer: 6.0.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@2.2.0) is-potential-custom-element-name: 1.0.1 lru-cache: 11.5.1 parse5: 8.0.1 @@ -38008,7 +38819,7 @@ snapshots: w3c-xmlserializer: 5.0.0 webidl-conversions: 8.0.1 whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 + whatwg-url: 16.0.1(@noble/hashes@2.2.0) xml-name-validator: 5.0.0 transitivePeerDependencies: - '@noble/hashes' @@ -38320,6 +39131,8 @@ snapshots: lunr@2.3.9: {} + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -38900,7 +39713,6 @@ snapshots: es-object-atoms: 1.1.2 has-symbols: 1.1.0 object-keys: 1.1.1 - optional: true object.entries@1.1.9: dependencies: @@ -38916,6 +39728,19 @@ snapshots: obug@2.1.3: {} + octane@0.1.21(@typescript-eslint/types@8.62.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)): + dependencies: + '@tsrx/core': 0.1.56(@typescript-eslint/types@8.62.0) + '@types/react': 19.2.16 + devalue: 5.8.2 + esrap: 2.3.0(@typescript-eslint/types@8.62.0) + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + vite: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) + transitivePeerDependencies: + - '@typescript-eslint/types' + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -39333,8 +40158,7 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - possible-typed-array-names@1.1.0: - optional: true + possible-typed-array-names@1.1.0: {} postcss-js@4.1.0(postcss@8.5.15): dependencies: @@ -39511,6 +40335,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proto-list@1.2.4: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -39744,6 +40570,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-is@19.2.7: {} react-number-format@5.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7): @@ -39894,7 +40722,6 @@ snapshots: get-proto: 1.0.1 gopd: 1.2.0 set-function-name: 2.0.2 - optional: true regexpu-core@6.4.0: dependencies: @@ -40213,7 +41040,6 @@ snapshots: call-bound: 1.0.4 es-errors: 1.3.0 is-regex: 1.2.1 - optional: true safer-buffer@2.1.2: {} @@ -40370,7 +41196,6 @@ snapshots: es-errors: 1.3.0 functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 - optional: true set-proto@1.0.0: dependencies: @@ -40690,7 +41515,6 @@ snapshots: dependencies: es-errors: 1.3.0 internal-slot: 1.1.0 - optional: true string-ts@2.3.1: {} @@ -40846,16 +41670,16 @@ snapshots: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.0) + '@sveltejs/acorn-typescript': 1.0.11(acorn@8.18.0) '@types/estree': 1.0.9 '@types/trusted-types': 2.0.7 - acorn: 8.16.0 + acorn: 8.18.0 aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.8.1 + devalue: 5.8.2 esm-env: 1.2.2 - esrap: 2.2.11(@typescript-eslint/types@8.62.0) + esrap: 2.3.0(@typescript-eslint/types@8.62.0) is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 @@ -40968,7 +41792,7 @@ snapshots: terser@5.46.2: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.16.0 + acorn: 8.18.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -41561,7 +42385,7 @@ snapshots: optionalDependencies: vite: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0) - vitest@4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@7.3.2(@types/node@26.0.0)(jiti@2.7.0)(less@4.6.4)(lightningcss@1.32.0)(sass@1.99.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)): + vitest@4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@7.3.2(@types/node@26.0.0)(jiti@2.7.0)(less@4.6.4)(lightningcss@1.32.0)(sass@1.99.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.9 '@vitest/mocker': 4.1.9(vite@7.3.2(@types/node@26.0.0)(jiti@2.7.0)(less@4.6.4)(lightningcss@1.32.0)(sass@1.99.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) @@ -41586,12 +42410,42 @@ snapshots: optionalDependencies: '@types/node': 26.0.0 '@vitest/coverage-v8': 4.1.9(vitest@4.1.9) - jsdom: 29.1.1 + jsdom: 29.1.1(@noble/hashes@2.2.0) transitivePeerDependencies: - msw optional: true - vitest@4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)): + vitest@4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.2 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.46.2)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.0.0 + '@vitest/coverage-v8': 4.1.9(vitest@4.1.9) + jsdom: 29.1.1(@noble/hashes@2.2.0) + transitivePeerDependencies: + - msw + optional: true + + vitest@4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.9 '@vitest/mocker': 4.1.9(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.16))(terser@5.46.2)(yaml@2.9.0)) @@ -41616,11 +42470,11 @@ snapshots: optionalDependencies: '@types/node': 26.0.0 '@vitest/coverage-v8': 4.1.9(vitest@4.1.9) - jsdom: 29.1.1 + jsdom: 29.1.1(@noble/hashes@2.2.0) transitivePeerDependencies: - msw - vitest@4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1)(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.99.0)(sugarss@5.0.1(postcss@8.5.13))(terser@5.46.2)(yaml@2.9.0)): + vitest@4.1.9(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.99.0)(sugarss@5.0.1(postcss@8.5.13))(terser@5.46.2)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.9 '@vitest/mocker': 4.1.9(vite@8.1.4(@types/node@26.0.0)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass@1.99.0)(sugarss@5.0.1(postcss@8.5.13))(terser@5.46.2)(yaml@2.9.0)) @@ -41645,7 +42499,7 @@ snapshots: optionalDependencies: '@types/node': 26.0.0 '@vitest/coverage-v8': 4.1.9(vitest@4.1.9) - jsdom: 29.1.1 + jsdom: 29.1.1(@noble/hashes@2.2.0) transitivePeerDependencies: - msw optional: true @@ -41704,6 +42558,8 @@ snapshots: vscode-uri@3.1.0: {} + vue-component-type-helpers@3.3.8: {} + vue-demi@0.14.10(vue@3.5.38(typescript@6.0.3)): dependencies: vue: 3.5.38(typescript@6.0.3) @@ -41941,8 +42797,8 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.16.0 - acorn-import-phases: 1.0.4(acorn@8.16.0) + acorn: 8.18.0 + acorn-import-phases: 1.0.4(acorn@8.18.0) browserslist: 4.28.2 chrome-trace-event: 1.0.4 enhanced-resolve: 5.23.0 @@ -41982,8 +42838,8 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.16.0 - acorn-import-phases: 1.0.4(acorn@8.16.0) + acorn: 8.18.0 + acorn-import-phases: 1.0.4(acorn@8.18.0) browserslist: 4.28.2 chrome-trace-event: 1.0.4 enhanced-resolve: 5.23.0 @@ -42022,8 +42878,8 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.16.0 - acorn-import-phases: 1.0.4(acorn@8.16.0) + acorn: 8.18.0 + acorn-import-phases: 1.0.4(acorn@8.18.0) browserslist: 4.28.2 chrome-trace-event: 1.0.4 enhanced-resolve: 5.23.0 @@ -42076,9 +42932,9 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 - whatwg-url@16.0.1: + whatwg-url@16.0.1(@noble/hashes@2.2.0): dependencies: - '@exodus/bytes': 1.15.1 + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) tr46: 6.0.0 webidl-conversions: 8.0.1 transitivePeerDependencies: @@ -42091,7 +42947,6 @@ snapshots: is-number-object: 1.1.1 is-string: 1.1.1 is-symbol: 1.1.1 - optional: true which-builtin-type@1.2.1: dependencies: @@ -42116,7 +42971,6 @@ snapshots: is-set: 2.0.3 is-weakmap: 2.0.2 is-weakset: 2.0.4 - optional: true which-typed-array@1.1.22: dependencies: @@ -42127,7 +42981,6 @@ snapshots: get-proto: 1.0.1 gopd: 1.2.0 has-tostringtag: 1.0.2 - optional: true which@2.0.2: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cc6e0ef9fa..0f6944a1e5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -10,6 +10,7 @@ overrides: '@tanstack/ember-table': 'workspace:*' '@tanstack/lit-table': 'workspace:*' '@tanstack/match-sorter-utils': 'workspace:*' + '@tanstack/octane-table': 'workspace:*' '@tanstack/preact-table-devtools': 'workspace:*' '@tanstack/preact-table': 'workspace:*' '@tanstack/react-table-devtools': 'workspace:*' @@ -47,6 +48,8 @@ allowBuilds: cleanupUnusedCatalogs: true minimumReleaseAge: 1440 +minimumReleaseAgeExclude: + - '@tanstack/octane-store@0.12.0' linkWorkspacePackages: true preferWorkspacePackages: true diff --git a/scripts/config.js b/scripts/config.js index a73ddf9edb..16d666202d 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -36,6 +36,10 @@ export const packages = [ name: '@tanstack/lit-table', packageDir: 'packages/lit-table', }, + { + name: '@tanstack/octane-table', + packageDir: 'packages/octane-table', + }, { name: '@tanstack/preact-table', packageDir: 'packages/preact-table', diff --git a/scripts/generateDocs.js b/scripts/generateDocs.js index 32de61ab8f..6e26a321bd 100644 --- a/scripts/generateDocs.js +++ b/scripts/generateDocs.js @@ -1,3 +1,4 @@ +import { readdir, readFile, writeFile } from 'node:fs/promises' import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { generateReferenceDocs } from '@tanstack/typedoc-config' @@ -34,6 +35,20 @@ await generateReferenceDocs({ outputDir: resolve(__dirname, '../docs/framework/preact/reference'), exclude: ['packages/table-core/**/*'], }, + { + name: 'octane-table', + // Octane ships authored .tsrx. TypeDoc reads the package's public + // declaration entry, which resolves the focused .tsrx.d.ts companions. + entryPoints: [ + resolve(__dirname, '../packages/octane-table/docs-entry.d.ts'), + ], + tsconfig: resolve( + __dirname, + '../packages/octane-table/tsconfig.docs.json', + ), + outputDir: resolve(__dirname, '../docs/framework/octane/reference'), + exclude: ['packages/table-core/**/*'], + }, { name: 'solid-table', entryPoints: [ @@ -102,6 +117,42 @@ await generateReferenceDocs({ ], }) +async function stripGeneratedTrailingWhitespace(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const entryPath = resolve(directory, entry.name) + + if (entry.isDirectory()) { + await stripGeneratedTrailingWhitespace(entryPath) + } else if (entry.name.endsWith('.md')) { + const source = await readFile(entryPath, 'utf8') + const formatted = source.replace(/[ \t]+$/gm, '') + + if (formatted !== source) { + await writeFile(entryPath, formatted) + } + } + } +} + +await stripGeneratedTrailingWhitespace(resolve(__dirname, '../docs/reference')) + +for (const framework of [ + 'react', + 'preact', + 'octane', + 'solid', + 'svelte', + 'vue', + 'angular', + 'lit', + 'ember', + 'alpine', +]) { + await stripGeneratedTrailingWhitespace( + resolve(__dirname, `../docs/framework/${framework}/reference`), + ) +} + console.log('\n✅ All markdown files have been processed!') process.exit(0) diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index cc9846e6a2..32659157e1 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -2,6 +2,7 @@ export default [ './packages/alpine-table/vite.config.ts', './packages/angular-table/vite.config.ts', './packages/lit-table/vite.config.ts', + './packages/octane-table/vite.config.ts', './packages/match-sorter-utils/vite.config.ts', './packages/react-table/vite.config.ts', './packages/solid-table/vite.config.ts', From f05ccdb3bd1a5a8767b03aeccc9d7a7cac184991 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Fri, 31 Jul 2026 11:00:51 -0500 Subject: [PATCH 2/3] add skills for octane --- README.md | 4 +- _artifacts/domain_map.yaml | 63 ++++- _artifacts/skill_spec.md | 24 +- _artifacts/skill_tree.yaml | 47 +++- knip.json | 1 + packages/alpine-table/README.md | 4 +- packages/angular-table/README.md | 4 +- packages/ember-table/README.md | 4 +- packages/lit-table/README.md | 4 +- packages/match-sorter-utils/README.md | 4 +- packages/octane-table/README.md | 4 +- packages/octane-table/package.json | 1 + .../skills/create-table-hook/SKILL.md | 173 +++++++++++++ .../skills/getting-started/SKILL.md | 170 +++++++++++++ .../octane-table/skills/table-state/SKILL.md | 165 +++++++++++++ packages/preact-table-devtools/README.md | 4 +- packages/preact-table/README.md | 4 +- packages/react-table-devtools/README.md | 4 +- packages/react-table/README.md | 4 +- packages/solid-table-devtools/README.md | 4 +- packages/solid-table/README.md | 4 +- packages/svelte-table/README.md | 4 +- packages/table-core/README.md | 4 +- packages/table-devtools/README.md | 4 +- packages/vue-table-devtools/README.md | 4 +- packages/vue-table/README.md | 4 +- prs-gpt-.md | 213 ++++++++++++++++ remaining-issues.md | 233 ++++++++++++++++++ 28 files changed, 1136 insertions(+), 26 deletions(-) create mode 100644 packages/octane-table/skills/create-table-hook/SKILL.md create mode 100644 packages/octane-table/skills/getting-started/SKILL.md create mode 100644 packages/octane-table/skills/table-state/SKILL.md create mode 100644 prs-gpt-.md create mode 100644 remaining-issues.md diff --git a/README.md b/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/_artifacts/domain_map.yaml b/_artifacts/domain_map.yaml index af0cf867f3..3ee4122471 100644 --- a/_artifacts/domain_map.yaml +++ b/_artifacts/domain_map.yaml @@ -11,7 +11,7 @@ library: meta: generated_by: '@tanstack/intent scaffold domain discovery' - date: '2026-07-29' + date: '2026-07-31' status: reviewed maintainer_review_pending: false phase_4_date: '2026-07-10' @@ -23,7 +23,7 @@ scope: - 'Use docs and examples for intended workflows, and direct exact API discovery to installed package declarations (dist/**/*.d.ts).' - 'Teach headless rendering, feature registration, row-model ownership, framework state, and v8-to-v9 changes as foundations.' included: - - 'All 17 public workspace packages.' + - 'All 18 public workspace packages.' - 'All 14 stock Table v9 features, custom features, TypeScript helpers, migrations, framework state, composable table hooks, Query, Virtual, Devtools, and fuzzy ranking.' - 'Renderer-owned CSS edge cases for sticky positioning, column widths, resizing, layout, and virtualization.' excluded: @@ -111,7 +111,8 @@ api_discovery_policy: feature_source: 'node_modules/@tanstack/table-core/dist/features//' ember_entrypoint: 'node_modules/@tanstack/ember-table/declarations/index.d.ts' angular_entrypoint: 'node_modules/@tanstack/angular-table/dist/types/' - fallback: 'If declarations are unavailable, resolve the installed package root and inspect published types; do not open package src/ or substitute v8 / another adapter API.' + octane_entrypoint: 'node_modules/@tanstack/octane-table/src/index.ts and matching *.tsrx.d.ts sidecars' + fallback: 'If declarations are unavailable, resolve the installed package root and inspect published types; do not open package src/ except for source-distributed Octane, or substitute v8 / another adapter API.' coverage: ignored_packages: @@ -1286,6 +1287,62 @@ skills: - 'Virtualizing raw input data instead of table.getRowModel().rows.' - 'Assuming Virtual or Table owns absolute positioning, measurement attributes, sticky CSS, or column widths.' + - slug: getting-started + package: '@tanstack/octane-table' + domain: framework-adapters + type: framework + framework: octane + purpose: 'Create and render an Octane v9 table with authored TSRX, stable inputs, keyed lists, and component-scoped render helpers.' + sources: + [ + 'TanStack/table:docs/framework/octane/quick-start.md', + 'TanStack/table:examples/octane/basic-use-table', + 'TanStack/table:packages/octane-table/src/index.ts', + 'TanStack/table:packages/octane-table/src/useTable.tsrx', + ] + failure_modes: + - 'Importing another framework adapter or translating React component-return semantics directly instead of using Octane TSRX component bodies.' + - 'Invoking FlexRender or other component-scoped helpers as plain functions instead of rendering them as components.' + - 'Recreating stable features, columns, or data on each render and causing unnecessary option and row-model work.' + + - slug: table-state + package: '@tanstack/octane-table' + domain: framework-adapters + type: framework + framework: octane + purpose: 'Use selected table.state, component-scoped Subscribe islands, controlled slices, and synchronous Octane Store atom ownership with commit-safe publication.' + sources: + [ + 'TanStack/table:docs/framework/octane/guide/table-state.md', + 'TanStack/table:examples/octane/basic-subscribe', + 'TanStack/table:examples/octane/basic-external-atoms', + 'TanStack/table:packages/octane-table/src/useTable.tsrx', + 'TanStack/table:packages/octane-table/src/Subscribe.tsrx', + ] + failure_modes: + - 'Reading atom/store snapshots in render as though they establish an Octane subscription.' + - 'Calling table.Subscribe as a normal function and sharing compiler slots instead of mounting an independent component scope.' + - 'Expecting controlled options.state to publish during render, or mixing it with an external atom that has ownership precedence.' + + - slug: create-table-hook + package: '@tanstack/octane-table' + domain: framework-adapters + type: framework + framework: octane + purpose: 'Build a typed reusable Octane table hook with stable App wrappers, registered components, and default or isolated contexts.' + sources: + [ + 'TanStack/table:docs/framework/octane/guide/composable-tables.md', + 'TanStack/table:docs/framework/octane/guide/table-context.md', + 'TanStack/table:examples/octane/composable-tables', + 'TanStack/table:packages/octane-table/src/createTableHook.tsrx', + 'TanStack/table:packages/octane-table/src/createTableHookContexts.ts', + ] + failure_modes: + - 'Using the app hook for one table when standalone useTable is clearer.' + - 'Reading a factory context outside its matching AppTable/AppCell/AppHeader provider or invoking a wrapper as a plain function.' + - 'Creating the factory in render and destabilizing wrapper, context, and registered-component identities.' + - slug: getting-started package: '@tanstack/ember-table' domain: framework-adapters diff --git a/_artifacts/skill_spec.md b/_artifacts/skill_spec.md index 5868132346..c4d9f2d96b 100644 --- a/_artifacts/skill_spec.md +++ b/_artifacts/skill_spec.md @@ -9,7 +9,7 @@ This specification is the generation contract for a deliberately smaller, foot-g ## Outcome -Generate 75 short package-local skills across all 17 public packages. A loaded skill should quickly do three things: +Generate 78 short package-local skills across all 18 public packages. A loaded skill should quickly do three things: 1. Correct the user or agent mental model. 2. Show the smallest reliable setup or decision pattern. @@ -47,10 +47,11 @@ Every skill that discusses APIs must tell the consuming agent how to inspect the - Stock feature API: node_modules/@tanstack/table-core/dist/features/FEATURE/. - Ember API: node_modules/@tanstack/ember-table/declarations/index.d.ts (and sibling `declarations/*.d.ts`). - Angular API: node_modules/@tanstack/angular-table/dist/types/\*.d.ts (bundled public API; do not expect `src/helpers/` under the published package). +- Octane API: node_modules/@tanstack/octane-table/src/index.ts, the matching `*.tsrx.d.ts` sidecar, and `src/types.ts` (the package intentionally distributes authored TSRX source). - Devtools API: node_modules/@tanstack/FRAMEWORK-table-devtools/dist/index.d.ts or @tanstack/table-devtools/dist/index.d.ts. - Fuzzy ranking API: node_modules/@tanstack/match-sorter-utils/dist/index.d.ts. -Do not open package `src/` under `node_modules` (it is not published). Do not direct agents to a GitHub main-branch source file when an installed package is available. Installed declarations keep guidance aligned with the consumer package version. +Do not open package `src/` under `node_modules` unless the package intentionally publishes source, as `@tanstack/octane-table` does. Do not direct agents to a GitHub main-branch source file when an installed package is available. Installed declarations and published source keep guidance aligned with the consumer package version. ## Skill writing contract @@ -241,7 +242,13 @@ Ember ships three: - table-state - create-table-hook -Do not add Query where no maintained adapter example exists. Do not add Alpine or Ember migration skills because neither has a v8 adapter journey to teach. Do not add Ember Query or Virtual skills until maintained adapter examples exist. Do not invent Preact virtualization examples; its Virtual skill should rely on the maintained adapter guide and installed APIs. +Octane ships three: + +- getting-started +- table-state +- create-table-hook + +Do not add Query where no maintained adapter example exists. Do not add Alpine, Ember, or Octane migration skills because none has a v8 adapter journey to teach. Do not add Ember or Octane Query or Virtual skills until maintained adapter examples exist. Do not invent Preact virtualization examples; its Virtual skill should rely on the maintained adapter guide and installed APIs. ### Devtools set (6) @@ -267,6 +274,7 @@ All Devtools skills must emphasize the required non-empty table options.key, lif | @tanstack/table-core | 21 | | @tanstack/react-table | 6 | | @tanstack/preact-table | 6 | +| @tanstack/octane-table | 3 | | @tanstack/solid-table | 6 | | @tanstack/svelte-table | 6 | | @tanstack/vue-table | 6 | @@ -281,7 +289,7 @@ All Devtools skills must emphasize the required non-empty table options.key, lif | @tanstack/vue-table-devtools | 1 | | @tanstack/angular-table-devtools | 1 | | @tanstack/match-sorter-utils | 1 | -| Total | 75 | +| Total | 78 | ## Framework distinctions that must survive generation @@ -297,6 +305,14 @@ All Devtools skills must emphasize the required non-empty table options.key, lif - Use the native Preact package, not React through preact/compat. - State selection and Subscribe resemble React but must use Preact adapter/store imports. +### Octane + +- The package distributes authored TSRX; consumer tooling must compile it with Octane, and components use TSRX component bodies plus keyed `@for` loops where appropriate. +- `useTable` stages fresh options for same-render reads, selects `table.state`, and publishes controlled state only from an accepted layout commit; abandoned work cannot notify the store. +- Render `table.Subscribe` and the createTableHook App wrappers as components so each has an independent Octane hook/context scope; never invoke them as plain functions. +- Use `@tanstack/octane-store` for external atoms. External atoms are synchronous owners and take precedence over controlled `options.state`. +- Native text inputs update on `onInput`; `onChange` follows native change timing rather than React's input-event alias. + ### Solid - createTable atoms are backed by Solid primitives; reads are reactive only inside tracked scopes. diff --git a/_artifacts/skill_tree.yaml b/_artifacts/skill_tree.yaml index 4d96285789..4f7d11f7f0 100644 --- a/_artifacts/skill_tree.yaml +++ b/_artifacts/skill_tree.yaml @@ -8,7 +8,7 @@ library: generated_from: domain_map: '_artifacts/domain_map.yaml' skill_spec: '_artifacts/skill_spec.md' -generated_at: '2026-07-29' +generated_at: '2026-07-31' status: reviewed reviewed_at: '2026-07-29' batch_review: true @@ -944,6 +944,51 @@ skills: - 'TanStack/table:examples/lit/virtualized-columns' - 'TanStack/table:examples/lit/virtualized-infinite-scrolling' + - name: 'Octane Getting Started' + slug: getting-started + type: framework + domain: framework-adapters + path: 'packages/octane-table/skills/getting-started/SKILL.md' + package: 'packages/octane-table' + description: 'Create an @tanstack/octane-table v9 table with useTable, tableFeatures, stable inputs, TSRX component bodies, keyed @for rendering, and FlexRender. Load when starting an Octane table or translating a React/Preact example without changing its behavior.' + requires: + ['@tanstack/table-core#core', '@tanstack/table-core#table-features'] + sources: + - 'TanStack/table:docs/framework/octane/quick-start.md' + - 'TanStack/table:examples/octane/basic-use-table' + - 'TanStack/table:packages/octane-table/src/index.ts' + - 'TanStack/table:packages/octane-table/src/useTable.tsrx' + + - name: 'Octane Table State' + slug: table-state + type: framework + domain: framework-adapters + path: 'packages/octane-table/skills/table-state/SKILL.md' + package: 'packages/octane-table' + description: 'Read and own Octane Table v9 state with useTable selectors, table.state, table.Subscribe, controlled slices, and @tanstack/octane-store atoms. Load for state ownership, render timing, snapshot-versus-subscription bugs, or fine-grained rendering.' + requires: ['@tanstack/table-core#core', 'getting-started'] + sources: + - 'TanStack/table:docs/framework/octane/guide/table-state.md' + - 'TanStack/table:examples/octane/basic-subscribe' + - 'TanStack/table:examples/octane/basic-external-atoms' + - 'TanStack/table:packages/octane-table/src/useTable.tsrx' + - 'TanStack/table:packages/octane-table/src/Subscribe.tsrx' + + - name: 'Octane createTableHook' + slug: create-table-hook + type: framework + domain: framework-adapters + path: 'packages/octane-table/skills/create-table-hook/SKILL.md' + package: 'packages/octane-table' + description: 'Create reusable Octane table infrastructure with createTableHook, useAppTable, createAppColumnHelper, registered components, stable App wrappers, and typed context hooks. Load for recurring conventions, scoped contexts, or prop drilling.' + requires: ['@tanstack/table-core#core', 'getting-started', 'table-state'] + sources: + - 'TanStack/table:docs/framework/octane/guide/composable-tables.md' + - 'TanStack/table:docs/framework/octane/guide/table-context.md' + - 'TanStack/table:examples/octane/composable-tables' + - 'TanStack/table:packages/octane-table/src/createTableHook.tsrx' + - 'TanStack/table:packages/octane-table/src/createTableHookContexts.ts' + - name: 'Ember Getting Started' slug: getting-started type: framework diff --git a/knip.json b/knip.json index aab19bc8a2..ac4eab13b4 100644 --- a/knip.json +++ b/knip.json @@ -38,6 +38,7 @@ }, "packages/octane-table": { "entry": [ + "docs-entry.d.ts!", "src/index.ts!", "src/Subscribe.tsrx!", "src/createTableHook.tsrx!", diff --git a/packages/alpine-table/README.md b/packages/alpine-table/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/alpine-table/README.md +++ b/packages/alpine-table/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/angular-table/README.md b/packages/angular-table/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/angular-table/README.md +++ b/packages/angular-table/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/ember-table/README.md b/packages/ember-table/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/ember-table/README.md +++ b/packages/ember-table/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/lit-table/README.md b/packages/lit-table/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/lit-table/README.md +++ b/packages/lit-table/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/match-sorter-utils/README.md b/packages/match-sorter-utils/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/match-sorter-utils/README.md +++ b/packages/match-sorter-utils/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/octane-table/README.md b/packages/octane-table/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/octane-table/README.md +++ b/packages/octane-table/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/octane-table/package.json b/packages/octane-table/package.json index 2590014b91..23497a128e 100644 --- a/packages/octane-table/package.json +++ b/packages/octane-table/package.json @@ -41,6 +41,7 @@ }, "files": [ "src", + "skills", "README.md" ], "scripts": { diff --git a/packages/octane-table/skills/create-table-hook/SKILL.md b/packages/octane-table/skills/create-table-hook/SKILL.md new file mode 100644 index 0000000000..e94eead5fa --- /dev/null +++ b/packages/octane-table/skills/create-table-hook/SKILL.md @@ -0,0 +1,173 @@ +--- +name: create-table-hook +description: > + Create reusable Octane table infrastructure with createTableHook, useAppTable, createAppColumnHelper, registered components, stable App wrappers, and typed context hooks. Load for recurring conventions, scoped contexts, or prop drilling. +metadata: + { + type: framework, + library: '@tanstack/octane-table', + library_version: '9.0.0-beta.65', + framework: octane, + } +requires: ['@tanstack/table-core#core', getting-started, table-state] +sources: + - 'TanStack/table:docs/framework/octane/guide/composable-tables.md' + - 'TanStack/table:docs/framework/octane/guide/table-context.md' + - 'TanStack/table:examples/octane/composable-tables' + - 'TanStack/table:packages/octane-table/src/createTableHook.tsrx' + - 'TanStack/table:packages/octane-table/src/createTableHookContexts.ts' +--- + +This skill builds on `@tanstack/table-core#core`, `getting-started`, and `table-state`. Use the app hook for repeated conventions; keep a one-off table on `useTable`. + +## Setup + +```tsrx +import { + createTableHook, + rowSelectionFeature, + tableFeatures, +} from '@tanstack/octane-table' + +export const { createAppColumnHelper, useAppTable, useTableContext } = + createTableHook({ + features: tableFeatures({ rowSelectionFeature }), + getRowId: (row: { id: string }) => row.id, + }) +``` + +Create the factory at module scope. Its wrappers have stable identities and read the latest table facade without remounting their child subtrees. + +## Core Patterns + +### Use the factory-bound helper + +```tsrx +type Person = { id: string; name: string } +const helper = createAppColumnHelper() +const columns = helper.columns([helper.accessor('name', { header: 'Name' })]) +``` + +### Consume typed context under its wrapper + +```tsrx +function RowCount() @{ + const table = useTableContext() + {String(table.getRowModel().rows.length)} +} + +function Grid() @{ + const table = useAppTable({ data, columns }) + +} +``` + +`AppTable` takes static children without a selector. Use function children when a selector supplies their value: + +```tsrx + state.rowSelection}> + {(rowSelection) => {String(Object.keys(rowSelection).length)}} + +``` + +Render `AppTable`, `AppCell`, `AppHeader`, `AppFooter`, and registered components with JSX. The wrappers establish context and independent Octane scopes. + +### Isolate genuinely nested table setups + +Default contexts are module-scoped and HMR-stable. Sibling tables are isolated by their providers, but nested factories can resolve a consumer to the inner provider. Create fresh contexts for distinct nested setups: + +```tsrx +import { + createTableHook, + createTableHookContexts, + rowSelectionFeature, + tableFeatures, +} from '@tanstack/octane-table' + +const features = tableFeatures({ rowSelectionFeature }) +const { tableContext, cellContext, headerContext } = + createTableHookContexts() + +export const app = createTableHook({ + features, + tableContext, + cellContext, + headerContext, +}) +``` + +Prefer hooks returned by `createTableHook` because their types include registered component maps. + +## Common Mistakes + +### MEDIUM Abstracting a single table + +Wrong: + +```tsrx +const app = createTableHook({ features: tableFeatures({}) }) +``` + +Correct: + +```tsrx +const table = useTable({ features, columns, data }) +``` + +The factory is valuable when it centralizes repeated policy, registered renderers, or typed context—not merely as another constructor name. + +Source: `docs/framework/octane/guide/composable-tables.md` + +### HIGH Reading outside the matching provider + +Wrong: + +```tsrx +function Grid() @{ + +} +``` + +Correct: + +```tsrx +function Grid() @{ + const table = useAppTable({ data, columns }) + +} +``` + +Factory context hooks throw actionable errors when the matching `AppTable`, `AppCell`, or `AppHeader` provider is missing. + +Source: `packages/octane-table/src/createTableHook.tsrx` + +### HIGH Recreating the factory during render + +Wrong: + +```tsrx +function Grid() @{ + const app = createTableHook({ features }) + const table = app.useAppTable({ data, columns }) + +} +``` + +Correct: + +```tsrx +const app = createTableHook({ features }) + +function Grid() @{ + const table = app.useAppTable({ data, columns }) + +} +``` + +A factory created during render makes hook configuration, wrappers, contexts, and registered component identities unstable. + +Source: `packages/octane-table/src/createTableHook.tsrx` + +## API Discovery + +Inspect `node_modules/@tanstack/octane-table/src/createTableHook.tsrx.d.ts`, `createTableHookContexts.ts`, and `types.ts` for exact return names, wrapper props, registries, and scoped context types. diff --git a/packages/octane-table/skills/getting-started/SKILL.md b/packages/octane-table/skills/getting-started/SKILL.md new file mode 100644 index 0000000000..a0f255469a --- /dev/null +++ b/packages/octane-table/skills/getting-started/SKILL.md @@ -0,0 +1,170 @@ +--- +name: getting-started +description: > + Create an @tanstack/octane-table v9 table with useTable, tableFeatures, stable inputs, TSRX component bodies, keyed @for rendering, and FlexRender. Load when starting an Octane table or translating a React/Preact example without changing its behavior. +metadata: + { + type: framework, + library: '@tanstack/octane-table', + library_version: '9.0.0-beta.65', + framework: octane, + } +requires: ['@tanstack/table-core#core', '@tanstack/table-core#table-features'] +sources: + - 'TanStack/table:docs/framework/octane/quick-start.md' + - 'TanStack/table:examples/octane/basic-use-table' + - 'TanStack/table:packages/octane-table/src/index.ts' + - 'TanStack/table:packages/octane-table/src/useTable.tsrx' +--- + +This skill builds on `@tanstack/table-core#core` and `@tanstack/table-core#table-features`. The adapter publishes authored TSRX, so the consuming app must compile it with Octane's Vite integration. + +## Setup + +```tsrx +import { createRoot, useState } from 'octane' +import { + createColumnHelper, + tableFeatures, + useTable, +} from '@tanstack/octane-table' + +type Person = { name: string } +const features = tableFeatures({}) +const helper = createColumnHelper() +const columns = helper.columns([helper.accessor('name', { header: 'Name' })]) + +function PeopleTable() @{ + const [data] = useState([{ name: 'Ada' }]) + const table = useTable({ features, columns, data }) + + + + @for (const group of table.getHeaderGroups(); key group.id) { + + @for (const header of group.headers; key header.id) { + + } + + } + + + @for (const row of table.getRowModel().rows; key row.id) { + + @for (const cell of row.getAllCells(); key cell.id) { + + } + + } + +
+} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Root element not found') +createRoot(rootElement).render(PeopleTable) +``` + +Configure the app with `octane()` in `vite.config.ts` and set the TSRX compiler's JSX import source to `octane`. + +## Core Patterns + +### Register only required plugins + +```tsrx +import { + createSortedRowModel, + rowSortingFeature, + tableFeatures, +} from '@tanstack/octane-table' + +const features = tableFeatures({ + rowSortingFeature, + sortedRowModel: createSortedRowModel(), +}) +``` + +### Keep features and column definitions module-stable + +```tsrx +const features = tableFeatures({}) +const columns = helper.columns([helper.accessor('name', { header: 'Name' })]) +``` + +Use `useMemo` when a definition genuinely depends on component state. Use `String(value)` for text interpolation when the value is not already a renderable string or number. + +### Render adapter helpers as components + +```tsrx + +``` + +`FlexRender` preserves numbers including `0`, descriptors, and Octane component functions. Rendering it as a component lets Octane create the correct component scope. + +## Common Mistakes + +### HIGH Importing a different framework adapter + +Wrong: + +```tsrx +import { useTable } from '@tanstack/react-table' +``` + +Correct: + +```tsrx +import { useTable } from '@tanstack/octane-table' +``` + +The Octane adapter uses Octane hooks, TSRX component scopes, and the matching Octane Store binding. + +Source: `packages/octane-table/src/useTable.tsrx` + +### HIGH Treating TSRX as React JSX + +Wrong: + +```tsrx +function PeopleTable() { + return +} +``` + +Correct: + +```tsrx +function PeopleTable() @{ +
+} +``` + +Author Octane components with TSRX component bodies. Prefer keyed `@for` loops for table rows, headers, and cells so identity survives updates. + +Source: `examples/octane/basic-use-table` + +### MEDIUM Recreating static inputs in render + +Wrong: + +```tsrx +const table = useTable({ + features: tableFeatures({}), + columns: [{ accessorKey: 'name' }], + data, +}) +``` + +Correct: + +```tsrx +const table = useTable({ features, columns, data }) +``` + +New feature and column identities cause needless option and row-model work. + +Source: `examples/octane/basic-use-table` + +## API Discovery + +Inspect `node_modules/@tanstack/octane-table/src/index.ts`, then the specific `*.tsrx.d.ts` sidecars and `types.ts`. This package intentionally ships authored source rather than a client-only `dist` build; follow core exports into installed `@tanstack/table-core/dist/`. diff --git a/packages/octane-table/skills/table-state/SKILL.md b/packages/octane-table/skills/table-state/SKILL.md new file mode 100644 index 0000000000..a51acdf4b4 --- /dev/null +++ b/packages/octane-table/skills/table-state/SKILL.md @@ -0,0 +1,165 @@ +--- +name: table-state +description: > + Read and own Octane Table v9 state with useTable selectors, table.state, table.Subscribe, controlled slices, and @tanstack/octane-store atoms. Load for state ownership, render timing, snapshot-versus-subscription bugs, or fine-grained rendering. +metadata: + { + type: framework, + library: '@tanstack/octane-table', + library_version: '9.0.0-beta.65', + framework: octane, + } +requires: ['@tanstack/table-core#core', getting-started] +sources: + - 'TanStack/table:docs/framework/octane/guide/table-state.md' + - 'TanStack/table:examples/octane/basic-subscribe' + - 'TanStack/table:examples/octane/basic-external-atoms' + - 'TanStack/table:packages/octane-table/src/useTable.tsrx' + - 'TanStack/table:packages/octane-table/src/Subscribe.tsrx' +--- + +This skill builds on `@tanstack/table-core#core` and `getting-started`. Octane has a render/commit split: options are visible to same-render reads, while controlled state is published only from an accepted layout commit. + +## State Mental Model + +Keep state internal unless another subsystem must read, persist, or drive it. With no `initialState`, `atoms`, `state`, or `on[State]Change`, the table owns every registered state slice. + +- `table.baseAtoms` are internal writable atoms initialized from resolved initial state. +- `table.atoms` are readonly derived atoms for the active owner of each registered slice. +- `table.store` is the readonly flat store assembled from those atoms. +- `table.state` contains only the result of the second `useTable` selector. + +Only registered features contribute state and types. If pagination is absent, add `rowPaginationFeature`; do not cast around the missing API. + +## Setup + +```tsrx +function PageStatus() @{ + const table = useTable( + { features, columns, data }, + (state) => ({ pagination: state.pagination }), + ) + + Page {String(table.state.pagination.pageIndex + 1)} +} +``` + +The selector shallow-gates owner rerenders and determines the shape of `table.state`. Omitting it selects all registered state. + +## Core Patterns + +### Fine-grained subscription islands + +```tsrx + + {(selection) => {String(Object.keys(selection).length)}} + +``` + +Render every `Subscribe` call site as a component. Each mounted island gets an independent hook scope and still receives the post-commit update when the owner drops its matching redundant notification. + +### External atom ownership + +```tsrx +import { useCreateAtom } from '@tanstack/octane-store' + +function Grid() @{ + const pagination = useCreateAtom({ pageIndex: 0, pageSize: 20 }) + const table = useTable({ features, columns, data, atoms: { pagination } }) + +} +``` + +External atoms are direct synchronous owners. They take precedence over `options.state`, and table API writes reach them. + +### Controlled slice ownership + +```tsrx +const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 20 }) +const table = useTable({ + features, + columns, + data, + state: { pagination }, + onPaginationChange: setPagination, +}) +``` + +Staged options and callbacks are available during the same render. Controlled state publication waits for the accepted layout commit, so suspended or abandoned work cannot notify subscribers with speculative state. + +## Choose State Ownership + +Use one owner per slice: + +- Prefer internal state and feature APIs when state is local to the table. +- Use `initialState` for starting/reset values; changing it later does not reset current state. +- Prefer a stable `@tanstack/octane-store` atom in `atoms` when state must be shared. Do not pair it with `on[State]Change`. +- Use `state.` plus its matching callback for a controlled slice. Feed the next value back and handle value-or-updater semantics. + +Use feature methods such as `setSorting`, `nextPage`, `toggleVisibility`, and `toggleSelected` for writes. The v8 global `onStateChange` option is gone. + +## Common Mistakes + +### HIGH Reading snapshots as subscriptions + +Wrong: + +```tsrx +const page = table.store.state.pagination.pageIndex +``` + +Correct: + +```tsrx +const page = table.state.pagination.pageIndex +``` + +Store and atom `.get()` reads are current snapshots. Selected `table.state` or a rendered `table.Subscribe` connects an Octane render. + +Source: `packages/octane-table/src/useTable.tsrx` + +### HIGH Calling Subscribe as a normal function + +Wrong: + +```tsrx +{table.Subscribe({ source: table.atoms.rowSelection, children: renderCount })} +``` + +Correct: + +```tsrx +{renderCount} +``` + +Direct invocation shares the owner's compiler slots instead of creating an independent component and hook scope. + +Source: `packages/octane-table/src/Subscribe.tsrx` + +### HIGH Controlling only the callback + +Wrong: + +```tsrx +useTable({ features, columns, data, onPaginationChange: setPagination }) +``` + +Correct: + +```tsrx +useTable({ + features, + columns, + data, + state: { pagination }, + onPaginationChange: setPagination, +}) +``` + +The callback must write into the value supplied for that controlled slice. + +Source: `docs/framework/octane/guide/table-state.md` + +## API Discovery + +Inspect `node_modules/@tanstack/octane-table/src/useTable.tsrx.d.ts`, `Subscribe.tsrx.d.ts`, and `types.ts`. Use `@tanstack/octane-store`, not another framework's Store hooks. diff --git a/packages/preact-table-devtools/README.md b/packages/preact-table-devtools/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/preact-table-devtools/README.md +++ b/packages/preact-table-devtools/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/preact-table/README.md b/packages/preact-table/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/preact-table/README.md +++ b/packages/preact-table/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/react-table-devtools/README.md b/packages/react-table-devtools/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/react-table-devtools/README.md +++ b/packages/react-table-devtools/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/react-table/README.md b/packages/react-table/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/react-table/README.md +++ b/packages/react-table/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/solid-table-devtools/README.md b/packages/solid-table-devtools/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/solid-table-devtools/README.md +++ b/packages/solid-table-devtools/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/solid-table/README.md b/packages/solid-table/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/solid-table/README.md +++ b/packages/solid-table/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/svelte-table/README.md b/packages/svelte-table/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/svelte-table/README.md +++ b/packages/svelte-table/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/table-core/README.md b/packages/table-core/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/table-core/README.md +++ b/packages/table-core/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/table-devtools/README.md b/packages/table-devtools/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/table-devtools/README.md +++ b/packages/table-devtools/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/vue-table-devtools/README.md b/packages/vue-table-devtools/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/vue-table-devtools/README.md +++ b/packages/vue-table-devtools/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/packages/vue-table/README.md b/packages/vue-table/README.md index 38d5c7ded0..9d6ed25cd4 100644 --- a/packages/vue-table/README.md +++ b/packages/vue-table/README.md @@ -35,6 +35,8 @@ > > - [Angular Table](https://tanstack.com/table/alpha/docs/framework/angular/angular-table) > - [Lit Table](https://tanstack.com/table/alpha/docs/framework/lit/lit-table) +> - [Octane Table](https://tanstack.com/table/beta/docs/framework/octane/quick-start) +> - [Preact Table](https://tanstack.com/table/beta/docs/framework/preact/quick-start) > - [React Table](https://tanstack.com/table/alpha/docs/framework/react/react-table) > - [Solid Table](https://tanstack.com/table/alpha/docs/framework/solid/solid-table) > - [Svelte Table](https://tanstack.com/table/alpha/docs/framework/svelte/svelte-table) @@ -44,7 +46,7 @@ A headless table library for building powerful datagrids with full control over markup, styles, and behavior. -- Framework‑agnostic core with bindings for React, Vue & Solid +- Framework-agnostic core with bindings for React, Preact, Octane, Vue, Solid, Svelte, Angular, Ember, Lit, and Alpine - 100% customizable — bring your own UI, components, and styles - Sorting, filtering, grouping, aggregation & row selection - Lightweight, virtualizable & server‑side friendly diff --git a/prs-gpt-.md b/prs-gpt-.md new file mode 100644 index 0000000000..46caa4f6b0 --- /dev/null +++ b/prs-gpt-.md @@ -0,0 +1,213 @@ +# TanStack Table open-PR closure audit + +Generated 2026-07-30 from a fresh scan of all 126 open pull requests in [`TanStack/table`](https://github.com/TanStack/table/pulls). + +## Result + +- **117 PRs can close now:** all 107 targeting `main` (v8), both targeting `v7`, and all 8 targeting the superseded v9 `alpha` branch. +- **9 PRs should remain open:** all target the current v9 `beta` branch. +- **2 current-beta PRs have merge conflicts:** [#6313](https://github.com/TanStack/table/pull/6313) and [#6443](https://github.com/TanStack/table/pull/6443). +- **1 additional beta PR has a failing required check:** [#6361](https://github.com/TanStack/table/pull/6361) is mergeable, but `@tanstack/table-core:test:types` fails. +- No PRs, issues, labels, reviews, or comments were changed as part of this audit. + +## Audit baseline + +- **Current/default branch:** `beta` at `79cbfb6722246baab6fa070966363700aca92298` (`v9.0.0-beta.62`) +- **v8 branch:** `main` at `d08af367e11c539bb7e18c2472aa761149ef6db6` (`v8.21.3`) +- **Superseded v9 branch:** `alpha` at `4f855fe4ed088c1d5d5149a3649518587b426d6c` (`v9.0.0-alpha.54`) +- **Legacy branch:** `v7` at `06703a56890122cedf1b2fa4b82982999537774e` (`v7.8.0`) +- **Evidence checked:** PR bodies, diffs, changed files, discussions/reviews, linked issue state, replacement PRs, current beta source, GitHub mergeability, draft state, and check rollups. +- **Branch rule:** close work based on `main`, `v7`, or `alpha`; retain work based on the current `beta` branch. +- **Conflict rule:** only GitHub's current `mergeable: false` / `mergeable_state: dirty` result is called a merge conflict. A failing or incomplete check is reported separately. + +## Summary + +| Category | Count | Recommended action | +| -------------------------------- | ------: | ----------------------------------------------------------------------------------- | +| Resolved or Superseded in V9 | 41 | Close; the behavior, docs, or architecture is already present or replaced in beta. | +| Revisit with a Fresh V9 PR | 19 | Close the stale PR, retain the issue/idea, and implement anew from beta if pursued. | +| Non-consequential to Port | 38 | Close; do not spend v9 migration effort on the stale docs/example/type cleanup. | +| Wrong Direction for the Library | 16 | Close with the current API/design boundary or supported alternative. | +| Too Conflict-heavy to Rescue | 3 | Close; conflict resolution would effectively be a rewrite. | +| Keep Open — Beta Clean | 4 | Keep; current beta target, clean merge state, and passing checks. | +| Keep Open — Beta Needs Attention | 3 | Keep; resolve CI, incomplete checks, or an outstanding design decision. | +| Keep Open — Beta Merge Conflicts | 2 | Keep, but rebase and resolve conflicts before further review. | +| **Total** | **126** | **117 close candidates; 9 keep open.** | + +## Suggested response language + +Use the response for the PR's category and add the PR-specific evidence from its audit line. + +### Resolved or Superseded in V9 + +> Thanks for the contribution. We are closing PRs against the pre-beta code lines as we consolidate development on v9 beta. This change is already covered by the current beta implementation or by a replacement PR, so there is nothing useful to port from this branch. We appreciate the work and report that helped identify it. + +### Revisit with a Fresh V9 PR + +> Thanks for working on this. The underlying issue/idea is legitimate, but this PR targets an obsolete code line and the current v9 architecture has moved too far for the patch to be carried forward safely. We are closing this PR while retaining the linked issue or audit note. If we pursue it, it should be a fresh PR from `beta` with a focused current-v9 regression test. + +### Non-consequential to Port + +> Thanks for the cleanup. We are closing stale PRs targeting pre-beta branches. Since this is limited to docs, examples, types, or minor cleanup in an area that has since moved, we are not going to spend migration effort porting it. A fresh current-branch PR is welcome if the same problem is still visible and consequential. + +### Wrong Direction for the Library + +> Thanks for exploring this. After reviewing it against the current v9 API and design boundaries, we do not want to take the library in this direction. We are closing the PR rather than porting it. The rationale or supported alternative is noted below. + +### Too Conflict-heavy to Rescue + +> Thanks for the substantial work here. This branch has diverged enough from the current v9 beta that resolving the conflicts would amount to rewriting the change, and the old diff is no longer a safe review unit. We are closing this PR. If the need still exists, please start with a narrowly scoped proposal or fresh PR from `beta`. + +## Close now + +### Resolved or Superseded in V9 (41) + +- [#5479 — Improve typing for RowSelectionState](https://github.com/TanStack/table/pull/5479) — Beta already types selection as `Record`, incorporating the exact true-only state model proposed here. +- [#5499 — fix: getIsSomeRowsSelected on all selection](https://github.com/TanStack/table/pull/5499) — Beta's some-selection predicate now treats every nonempty selection, including all rows selected, consistently; the v8 implementation no longer applies. +- [#5519 — ensure columnPinning.[property] is not accessed unsafely](https://github.com/TanStack/table/pull/5519) — V9 replaced the v8/Qwik state path with registered atoms and logical start/end pinning; the unsafe optional physical state access is gone. +- [#5667 — Fix: allow TValue to be specified in display and group column helpers](https://github.com/TanStack/table/pull/5667) — V9 rewrote column-helper and feature typing, replacing the v8 declaration-merging variance failure this patch works around. +- [#5747 — Improve contextDocument handling in ColumnSizing feature](https://github.com/TanStack/table/pull/5747) — Duplicate of merged [#5989](https://github.com/TanStack/table/pull/5989), which resolved the document-selection issue on the v8 line. +- [#5754 — Refactor example and fix ESLint error](https://github.com/TanStack/table/pull/5754) — The old editable-data example structure does not exist in beta; current examples use the v9 adapter/rendering architecture. +- [#5789 — docs: Fixed links in Columns Definitions Guide](https://github.com/TanStack/table/pull/5789) — The current docs tree and later link cleanup already contain valid column-definition targets. +- [#5879 — fix(react-table): fix improper state update on unmounted component](https://github.com/TanStack/table/pull/5879) — V9's React adapter uses TanStack Store subscriptions rather than the v8 local-setState path behind [#5026](https://github.com/TanStack/table/issues/5026). +- [#5902 — Check whether filter value is an array](https://github.com/TanStack/table/pull/5902) — Beta already guards the numeric-range auto-remove path with `Array.isArray`, resolving [#5901](https://github.com/TanStack/table/issues/5901). +- [#5933 — docs: Fix links in custom-features.md](https://github.com/TanStack/table/pull/5933) — The v9 guide tree and later mass link migration supersede these v8-relative link edits. +- [#5934 — Fix Table API doc link](https://github.com/TanStack/table/pull/5934) — Current v9 table guides point at the rebuilt core API documentation. +- [#5937 — docs: make global filter work in row selection example](https://github.com/TanStack/table/pull/5937) — Beta's row-selection example registers global filtering and has end-to-end coverage for the filter input. +- [#5963 — Update column-sizing.md](https://github.com/TanStack/table/pull/5963) — The broken v8 relative links were superseded by the later documentation link migration. +- [#6005 — fix: export GroupColumnDefBase](https://github.com/TanStack/table/pull/6005) — V9's public `GroupColumnDef` declarations emit without leaking the private v8 base type, resolving [#6004](https://github.com/TanStack/table/issues/6004) without this export. +- [#6015 — Update column-sizing.md](https://github.com/TanStack/table/pull/6015) — This is another superseded attempt at the v8 documentation-link problem handled by the later link migration. +- [#6017 — fix(table-core): correctly count only truthy selected rows in getIsSomeRowsSelected](https://github.com/TanStack/table/pull/6017) — Beta's true-only selection-state type and updated predicate remove false-valued IDs from this calculation. +- [#6026 — fix: Avoid duplicate call to autoreset by removing the wrap in `_queue`](https://github.com/TanStack/table/pull/6026) — V9 removed the v8 microtask `_queue` architecture that caused [#6025](https://github.com/TanStack/table/issues/6025). +- [#6055 — Add missing equalsStringSenstivie](https://github.com/TanStack/table/pull/6055) — Beta already exports and registers `equalsStringSensitive`. +- [#6056 — fix(table-core): Correct `getAutoSortingFn` logic when row count is less then `10`](https://github.com/TanStack/table/pull/6056) — Beta samples with `slice(0, 10)`, so [#4908](https://github.com/TanStack/table/issues/4908) is already fixed. +- [#6067 — fix(doc): column filtering typo](https://github.com/TanStack/table/pull/6067) — The exact [#6064](https://github.com/TanStack/table/issues/6064) wording was already fixed by merged [#5509](https://github.com/TanStack/table/pull/5509). +- [#6072 — docs: fix non-existent example value for globalFilterFn](https://github.com/TanStack/table/pull/6072) — V9 replaced the affected guide and filtering examples with its feature-registered API. +- [#6084 — fix: clear multi-sort when clicking column without shift key](https://github.com/TanStack/table/pull/6084) — Current beta PR [#6431](https://github.com/TanStack/table/pull/6431) is the focused v9 replacement for the same canonical regression in [#4309](https://github.com/TanStack/table/issues/4309). +- [#6088 — docs: add missing import type for TableMeta module declaration](https://github.com/TanStack/table/pull/6088) — V9 uses per-table `tableMeta` typing instead of the affected global v8 `TableMeta` declaration-merging pattern. +- [#6094 — Fix calculation of leafRows in getGroupedRowModel](https://github.com/TanStack/table/pull/6094) — V9's grouping rewrite already corrected the depth-dependent leaf-row calculation from [#6093](https://github.com/TanStack/table/issues/6093). +- [#6098 — fix(table-core): CoreOptions columns and data are readonly](https://github.com/TanStack/table/pull/6098) — Beta's core options already accept readonly data and column-definition arrays. +- [#6134 — fix(table-core): Pass column to resolveFilterValue](https://github.com/TanStack/table/pull/6134) — V9 replaced the mismatched v8 signature with an intentional one-argument `resolveFilterValue` contract. +- [#6154 — docs: fix broken global filter example link](https://github.com/TanStack/table/pull/6154) — Superseded by merged [#6231](https://github.com/TanStack/table/pull/6231), which fixed the guide links comprehensively. +- [#6156 — fix(core): avoid Date.now() calls in memo when debug is disabled](https://github.com/TanStack/table/pull/6156) — V9 only performs timing work when debugging is enabled and no longer has the unconditional v8 render-time `Date.now()` path. +- [#6161 — fix(table-core): clear stale columnFiltersMeta in getFilteredRowModel](https://github.com/TanStack/table/pull/6161) — Beta resets `columnFilters` and `columnFiltersMeta` for every flat row on each filtered-model pass. +- [#6168 — fix: add variance annotations to fix file-order-dependent type errors](https://github.com/TanStack/table/pull/6168) — V9's rewritten recursive table/column types already carry explicit variance annotations, resolving [#6167](https://github.com/TanStack/table/issues/6167). +- [#6171 — Fix broken documentation link for global filters example](https://github.com/TanStack/table/pull/6171) — Another duplicate superseded by merged [#6231](https://github.com/TanStack/table/pull/6231). +- [#6187 — fix(table-core): consistent alphanumeric sorting for mixed letter/digit strings](https://github.com/TanStack/table/pull/6187) — V9 replaced the misaligned regex chunk comparator and covers the [#6174](https://github.com/TanStack/table/issues/6174) mixed-character case. +- [#6193 — docs: fix broken filters-global example link in global filtering guide](https://github.com/TanStack/table/pull/6193) — A third duplicate superseded by merged [#6231](https://github.com/TanStack/table/pull/6231). +- [#6208 — fix(pagination): autoResetPageIndex should reset to 0, not initialState pageIndex](https://github.com/TanStack/table/pull/6208) — Merged beta PR [#6411](https://github.com/TanStack/table/pull/6411) implements the current-v9 fix and closed [#6207](https://github.com/TanStack/table/issues/6207). +- [#6239 — fix: preserve undefined in deep accessor types](https://github.com/TanStack/table/pull/6239) — Merged beta PR [#6444](https://github.com/TanStack/table/pull/6444) is the current-v9 version and closed [#6238](https://github.com/TanStack/table/issues/6238). +- [#6282 — fix(grouping): aggregate grouped columns at shallower group levels](https://github.com/TanStack/table/pull/6282) — Merged beta PR [#6421](https://github.com/TanStack/table/pull/6421) restored the required aggregation context for [#6228](https://github.com/TanStack/table/issues/6228). +- [#6337 — fix(table-core): prototype-named column ids poisoning row value caches](https://github.com/TanStack/table/pull/6337) — Beta already creates null-prototype maps and uses safe `hasOwn` checks for row value caches. +- [#6354 — docs: fix missing word in column filtering guide](https://github.com/TanStack/table/pull/6354) — The same missing word was fixed earlier by merged [#5509](https://github.com/TanStack/table/pull/5509). +- [#6451 — docs: complete truncated sentence in columns guide](https://github.com/TanStack/table/pull/6451) — The beta Columns Guide already contains the completed sentence and corrected “rendering” spelling, resolving [#5761](https://github.com/TanStack/table/issues/5761). +- [#5942 — Don't try to @render svelte components in FlexRender](https://github.com/TanStack/table/pull/5942) (`alpha`) — Beta replaced the alpha Svelte `FlexRender.svelte` implementation with the current Svelte 5 adapter/rendering path. +- [#6252 — fix(table-core): scan rows for first non-nullish value in getColumnCanGlobalFilter](https://github.com/TanStack/table/pull/6252) (`alpha`) — Current beta PR [#6438](https://github.com/TanStack/table/pull/6438) is the focused replacement with a current regression test. + +### Revisit with a Fresh V9 PR (19) + +- [#5031 — Fix an issue where pageIndex becomes invalid when data is changed](https://github.com/TanStack/table/pull/5031) — [#4994](https://github.com/TanStack/table/issues/4994) still fails a focused beta regression when shrunken data leaves an invalid page index; keep the issue and implement the clamp against v9's pagination atoms. +- [#5373 — fix(table-core): Avoid process not defined in environments not setting a process variable](https://github.com/TanStack/table/pull/5373) — The underlying unbundled-ESM concern remains tracked by [#6078](https://github.com/TanStack/table/issues/6078), but this old patch overlaps #6185 and predates v9's reorganized source; retain one current reproduction and start fresh. +- [#5414 — feat: add autoResetSorting](https://github.com/TanStack/table/pull/5414) — The opt-in use case may be valid, but the v8 patch defaults the reset on for client sorting despite the maintainer discussion and has no current state-scheduling tests; reconsider as a beta-first design. +- [#5582 — fix: invalidate cached values after updating accessor functions](https://github.com/TanStack/table/pull/5582) — [#5363](https://github.com/TanStack/table/issues/5363) still fails on beta, but v9's row/column construction and caches differ substantially; preserve the regression and rewrite from beta. +- [#5790 — fix(table-core): ensure isSubRowSelected returns false if no subRows are selectable](https://github.com/TanStack/table/pull/5790) — [#5173](https://github.com/TanStack/table/issues/5173) remains a verified beta bug; consolidate this and #6177 into one fresh v9 predicate fix with disabled-descendant coverage. +- [#5823 — fix: crash when ungrouping after aggregation row was pinned](https://github.com/TanStack/table/pull/5823) — [#5822](https://github.com/TanStack/table/issues/5822) still throws on beta when a stale synthetic group ID remains pinned; the v9 row-pinning/grouping code needs a fresh patch. +- [#5890 — Add `header.getAfter` API](https://github.com/TanStack/table/pull/5890) — Beta has logical start/end pinning and `column.getAfter`, but no header equivalent; if grouped end-pinning still needs it, design and test a logical v9 API rather than porting the physical-right v8 patch. +- [#5978 — fix: Duplicate sub-rows in pagination row model](https://github.com/TanStack/table/pull/5978) — [#5833](https://github.com/TanStack/table/issues/5833) still produces duplicate beta `flatRows`; carry the failing regression into a fresh v9 pagination-model fix. +- [#6075 — fix: retain columnFiltersMeta when leaf row filtering](https://github.com/TanStack/table/pull/6075) — [#6074](https://github.com/TanStack/table/issues/6074) still loses beta filter metadata after `filterFromLeafRows`; reimplement against v9's new filter-row utilities. +- [#6116 — [BUG] ignore non expandable rows when the expanded state changes](https://github.com/TanStack/table/pull/6116) — [#6115](https://github.com/TanStack/table/issues/6115) remains plausible in beta, but needs a current expansion-depth regression and an explicit semantic decision before a new implementation. +- [#6176 — Added server.allowed hosts to vite config](https://github.com/TanStack/table/pull/6176) — [#6175](https://github.com/TanStack/table/issues/6175) remains a valid examples-infrastructure issue, but the 73 v8 example configs are no longer the current set; generate a focused beta-era configuration change. +- [#6177 — fix: table-core's getIsAllSubRowsSelected returns false when subrows are unselectable](https://github.com/TanStack/table/pull/6177) — This is the second stale approach to verified beta issue [#5173](https://github.com/TanStack/table/issues/5173); close both old PRs and use one fresh beta fix. +- [#6178 — fix(table-core): respect custom filterFn.autoRemove in shouldAutoRemoveFilter](https://github.com/TanStack/table/pull/6178) — [#6101](https://github.com/TanStack/table/issues/6101) is a valid API inconsistency; consolidate this with duplicate #6195 into a tested beta implementation. +- [#6184 — fix(table-core): skip state update in toggleExpanded when state is unchanged](https://github.com/TanStack/table/pull/6184) — [#6136](https://github.com/TanStack/table/issues/6136) needs a current React commit/render regression before changing beta; consolidate this with duplicate #6194. +- [#6185 — fix(table-core): guard process.env checks for non-bundled environments](https://github.com/TanStack/table/pull/6185) — This is the newer duplicate of #5373 for [#6078](https://github.com/TanStack/table/issues/6078); verify the published beta browser export, then write one current patch if it still fails. +- [#6186 — fix(vue-table): return null for empty string in FlexRender to prevent hydration mismatch](https://github.com/TanStack/table/pull/6186) — [#6077](https://github.com/TanStack/table/issues/6077) is plausible, but the Vue v9 adapter was rewritten; require a current SSR hydration regression before a fresh fix. +- [#6194 — fix: skip toggleExpanded when state already matches desired value](https://github.com/TanStack/table/pull/6194) — Duplicate v8 approach to [#6136](https://github.com/TanStack/table/issues/6136); use the same current-v9 investigation/fresh-PR path as #6184. +- [#6195 — fix: respect custom filterFn.autoRemove over default empty string check](https://github.com/TanStack/table/pull/6195) — Duplicate v8 approach to valid issue [#6101](https://github.com/TanStack/table/issues/6101); use the same fresh-beta path as #6178. +- [#6417 — fix(table-core): refresh faceted values on option updates](https://github.com/TanStack/table/pull/6417) — [#6081](https://github.com/TanStack/table/issues/6081) still fails a focused beta regression, but this PR targets `main`; recreate its option-update cache test and fix from beta. + +### Non-consequential to Port (38) + +- [#5729 — docs(expansion/table): tip use table option getRowId if data may change](https://github.com/TanStack/table/pull/5729) — V8-only documentation for a long-closed v7-era issue; no v9 code or urgent documentation gap. +- [#5750 — Fix: Corrected typo in example code](https://github.com/TanStack/table/pull/5750) — One-character v8 guide typo in documentation that has since been rewritten. +- [#5862 — docs: improve vanilla js installation docs](https://github.com/TanStack/table/pull/5862) — V8-only installation wording, with no code or current beta reproduction. +- [#5878 — fix: improve typing on makeStateUpdater function](https://github.com/TanStack/table/pull/5878) — Internal v8 type-guard/cast cleanup; v9 replaced the updater implementation. +- [#5886 — Update fuzzy-filtering.md](https://github.com/TanStack/table/pull/5886) — A one-line wording edit in an old guide. +- [#5915 — refactor: remove unnecessary code](https://github.com/TanStack/table/pull/5915) — Unused import and ref dependency cleanup in two v8 examples. +- [#5960 — docs: typo in guide/sorting.md](https://github.com/TanStack/table/pull/5960) — Single v8 documentation typo. +- [#5967 — Improve type for Editable Data React example](https://github.com/TanStack/table/pull/5967) — Example-only TypeScript narrowing for an example absent from beta. +- [#5982 — Fix typo in table-state.md](https://github.com/TanStack/table/pull/5982) — Single v8 React guide typo. +- [#6020 — Improve match-sorter-util performance by replacing string comparisons](https://github.com/TanStack/table/pull/6020) — Seven-line micro-optimization without a benchmark or linked user-visible regression; not worth a stale-branch port. +- [#6023 — docs(typo): fix typo](https://github.com/TanStack/table/pull/6023) — One-word v8 tables-guide edit. +- [#6027 — Fixed typo `it’s` to `its`](https://github.com/TanStack/table/pull/6027) — One-character Vue documentation edit. +- [#6031 — Fix DeepKeys to handle arrays properly](https://github.com/TanStack/table/pull/6031) — Type-only v8 accessor-path expansion with no linked regression or consumer test to justify porting. +- [#6037 — docs: add forgotten “mean” to column filtering docs](https://github.com/TanStack/table/pull/6037) — Single missing word in old docs. +- [#6038 — docs: remove unused footer column def](https://github.com/TanStack/table/pull/6038) — Six-line cleanup in a v8 example. +- [#6045 — docs: typos in guide/table-state.md](https://github.com/TanStack/table/pull/6045) — Three small wording fixes in a v8 guide. +- [#6052 — fix: add ref to drag handle button for improved DnD functionality](https://github.com/TanStack/table/pull/6052) — Example-only DnD wiring owned by the renderer/integration, not Table core. +- [#6053 — Fix writing in column-filtering.md](https://github.com/TanStack/table/pull/6053) — Two grammar changes in old docs. +- [#6054 — docs: fixed API docs link](https://github.com/TanStack/table/pull/6054) — Source-comment link correction with no runtime impact. +- [#6095 — fix: next section is actually previous section](https://github.com/TanStack/table/pull/6095) — One-word cross-reference correction. +- [#6103 — Update column-filtering.md](https://github.com/TanStack/table/pull/6103) — Removes one duplicated word from a v8 guide. +- [#6105 — Update headers.md](https://github.com/TanStack/table/pull/6105) — Documentation-only `colspan` to `colSpan` casing. +- [#6107 — Update table-state.md, fix parens typo](https://github.com/TanStack/table/pull/6107) — One-character Vue guide correction. +- [#6108 — docs: fix typo in column filtering guide](https://github.com/TanStack/table/pull/6108) — One grammar edit in the v8 guide. +- [#6110 — docs(expanding): clarify description of autoResetExpanded setting](https://github.com/TanStack/table/pull/6110) — V8-only option wording; v9 reset behavior and docs have been rebuilt. +- [#6125 — Fix syntax error in pagination example](https://github.com/TanStack/table/pull/6125) — Duplicate one-character Vue example correction in the same area as #6107. +- [#6146 — Fix punctuation in introduction.md](https://github.com/TanStack/table/pull/6146) — Punctuation-only old-docs change. +- [#6147 — Fix typo in column filtering documentation](https://github.com/TanStack/table/pull/6147) — Grammar-only old-docs change. +- [#6160 — docs: fix a few small typos](https://github.com/TanStack/table/pull/6160) — Three small wording changes across v8 guides. +- [#6163 — docs: fix TanStack Start link in README](https://github.com/TanStack/table/pull/6163) — One ecosystem link; harmless to close with the v8 batch and re-open from beta only if still prioritized. +- [#6169 — Fix TanStack virtual example link](https://github.com/TanStack/table/pull/6169) — One example comment link. +- [#6204 — Docs: fix what seems to be a copy-paste error](https://github.com/TanStack/table/pull/6204) — Small v8 cells-guide correction. +- [#6205 — Docs: fix typo in table state documentation](https://github.com/TanStack/table/pull/6205) — One Angular guide typo. +- [#6210 — Docs: fix typo in column filtering documentation](https://github.com/TanStack/table/pull/6210) — One old-guide typo. +- [#5920 — chore(svelte-table): cleanup package.json](https://github.com/TanStack/table/pull/5920) (`alpha`) — Package-manifest cleanup against a superseded alpha package layout; no behavioral fix. +- [#6203 — update coderabbit logo](https://github.com/TanStack/table/pull/6203) (`alpha`) — Sponsor/logo presentation only. +- [#6155 — docs(v7): use github source link instead of broken v7 doc site](https://github.com/TanStack/table/pull/6155) (`v7`) — Legacy docs-only link; the associated [#6008](https://github.com/TanStack/table/issues/6008) was closed as completed on 2026-07-27 with the archived Netlify URL documented. +- [#6165 — Fix link to FAQ in useTable documentation](https://github.com/TanStack/table/pull/6165) (`v7`) — One link in end-of-life v7 documentation. + +### Wrong Direction for the Library (16) + +- [#5274 — Do not select subrows if enableSubRowSelection is false](https://github.com/TanStack/table/pull/5274) — `enableSubRowSelection` controls cascading from a toggled parent; select-all intentionally evaluates each independently selectable row. Use `enableRowSelection` to exclude rows. +- [#5401 — update logic to fix selection of disabled rows](https://github.com/TanStack/table/pull/5401) — Disabled is a current interaction capability, not immutable selection state; bulk deselection must still be able to clear controlled/persisted IDs. +- [#5514 — reduce memory leak by remove memo function](https://github.com/TanStack/table/pull/5514) — Blanket removal of feature memoization trades away computation without isolating a leak; v9 addressed allocations through shared prototypes and targeted row/cell architecture changes. +- [#5697 — fix: array filterFns](https://github.com/TanStack/table/pull/5697) — `arrIncludes`, `arrIncludesAll`, and `arrIncludesSome` intentionally filter array-valued cells. Scalar cells should use `equals` or a custom function. +- [#5950 — add filterRowsUtils export](https://github.com/TanStack/table/pull/5950) — The recursive filter utility is an implementation detail tied to internal row-model invariants; custom row models should not depend on that unstable private surface. +- [#5964 — Remove console.error log from table.getColumn(id)](https://github.com/TanStack/table/pull/5964) — V9 deliberately retains a development-only missing-column warning to expose stale IDs while still returning `undefined`. +- [#5974 — support unsetting getPaginationRowModel](https://github.com/TanStack/table/pull/5974) — Dynamic client/server pagination should use `manualPagination`; treating removal of a row-model factory as a runtime mode switch complicates cache ownership. +- [#5996 — Prevent deletion of non-selectable rows on RowSelectionChange](https://github.com/TanStack/table/pull/5996) — Same capability-versus-immutability problem as #5401: non-selectable rows may still need their stale selection IDs cleared. +- [#6062 — feat(RowSelection): support indeterminate states](https://github.com/TanStack/table/pull/6062) — The proposal makes a parent's explicit selection ID a derived mirror of descendants. Current Table semantics expose explicit and descendant predicates separately so renderers can combine them. +- [#6085 — docs: add Excel-like sorting guide for null/undefined handling](https://github.com/TanStack/table/pull/6085) — Excel-style coercion and empty-value ordering are application-specific collation policy; use a custom `sortingFn` rather than establish it as an official Table behavior. +- [#6120 — fix API to calculate max expanded row depth](https://github.com/TanStack/table/pull/6120) — Collapsing a parent should not erase descendant expansion choices; preserving them allows nested state to return when the parent is reopened. +- [#6123 — add existingTable param to useReactTable](https://github.com/TanStack/table/pull/6123) — V9 supports external ownership through atoms/stores; threading an existing instance through every framework adapter couples hook lifecycles and includes removed Qwik code. +- [#6129 — toggleAllRowsSelected should respect row selection rules](https://github.com/TanStack/table/pull/6129) — `enableRowSelection` gates whether a row can be interactively selected, not whether a bulk command may remove an already-stored ID. +- [#6240 — type column contexts with React table](https://github.com/TanStack/table/pull/6240) (`alpha`) — Core cell/header contexts must remain framework-neutral. React's `Subscribe` helper is intentionally an adapter component, not part of the core `Table` context type. +- [#6241 — added IgnoreDiacritics sort functions](https://github.com/TanStack/table/pull/6241) (`alpha`) — Four built-ins encode locale-sensitive collation policy and expand the registry; applications can supply an `Intl.Collator`-backed custom sorting function. +- [#6295 — clamp column size during resize drag](https://github.com/TanStack/table/pull/6295) (`alpha`) — Current v8/v9 getters already clamp committed sizes to `minSize`/`maxSize`, and [#5997](https://github.com/TanStack/table/issues/5997) could not be reproduced under the supported rendering path. + +### Too Conflict-heavy to Rescue (3) + +- [#5865 — docs: update router example and types](https://github.com/TanStack/table/pull/5865) — Conflicting 2025 dependency/example migration with 775 additions, 189 deletions, a lockfile rewrite, and an obsolete router API; rebuilding the current beta example is safer. +- [#6086 — feat: add sortEmpty option for comprehensive empty value handling](https://github.com/TanStack/table/pull/6086) — Conflicting 11-file, 1,092-line API/deprecation/example change. It also needs a new design decision on app-specific empty-value policy, so conflict resolution would not rescue a reviewable patch. +- [#6242 — WIP: adapt solid-table to Solid v2 beta reactivity model](https://github.com/TanStack/table/pull/6242) (`alpha`) — Conflicting draft spanning 83 files with 1,050 additions and 454 deletions against an older Solid v2 beta and v9 alpha; any remaining Solid work must start from current beta. + +## Keep open + +### Beta Clean (4) + +- [#6431 — fix sorting replacement in single-sort mode](https://github.com/TanStack/table/pull/6431) — Draft, mergeable/clean, and all checks pass. It fixes the verified [#4309](https://github.com/TanStack/table/issues/4309) beta regression with focused tests; keep and move out of draft when review-ready. +- [#6438 — fix global filtering with undefined first values](https://github.com/TanStack/table/pull/6438) — Draft, mergeable/clean, and all checks pass. It fixes verified [#4783](https://github.com/TanStack/table/issues/4783) and supersedes alpha #6252. +- [#6439 — honor explicit global filter opt-in](https://github.com/TanStack/table/pull/6439) — Draft, mergeable/clean, and all checks pass. It fixes verified object-valued global filtering in [#4673](https://github.com/TanStack/table/issues/4673). +- [#6442 — toggle group column visibility](https://github.com/TanStack/table/pull/6442) — Draft, mergeable/clean, and all checks pass. It supplies the beta design/test for valid group visibility issue [#5497](https://github.com/TanStack/table/issues/5497). + +### Beta Needs Attention (3) + +- [#6361 — include sub-rows in flatRows when maxLeafRowFilterDepth truncates recursion](https://github.com/TanStack/table/pull/6361) — Keep: non-draft and mergeable, and it targets verified [#5987](https://github.com/TanStack/table/issues/5987). The required `Test` check currently fails at `@tanstack/table-core:test:types`; update to current beta types and rerun. +- [#6385 — Allow Manual Override of Pagination canNextPage/canPreviousPage](https://github.com/TanStack/table/pull/6385) — Keep: draft and mergeable with no conflict. GitHub reports `UNSTABLE` and the normal full test/preview rollup is absent; it also needs maintainer agreement on whether navigation capability belongs in pagination state or options. +- [#6445 — perf: compute NODE_ENV up-front & flip opt-in](https://github.com/TanStack/table/pull/6445) — Keep: draft and mergeable with no conflict. GitHub reports `UNSTABLE` with only review/security checks present; decide the `!== 'production'` behavior, then run the full required suite. + +### Beta Merge Conflicts (2) + +- [#6313 — exclude non-numeric values from inNumberRange filter](https://github.com/TanStack/table/pull/6313) — Keep, but GitHub reports `CONFLICTING`/`dirty`. It fixes verified [#6007](https://github.com/TanStack/table/issues/6007); rebase onto current beta, preserve the finite-number regression coverage, and rerun checks. +- [#6443 — reset expansion when data changes](https://github.com/TanStack/table/pull/6443) — Keep, but the draft is `CONFLICTING`/`dirty` and its `Test` check fails. It fixes verified [#5801](https://github.com/TanStack/table/issues/5801); rebase onto current beta and revalidate the core-row-model reset hook. diff --git a/remaining-issues.md b/remaining-issues.md new file mode 100644 index 0000000000..5ec8b7f955 --- /dev/null +++ b/remaining-issues.md @@ -0,0 +1,233 @@ +# TanStack Table remaining open-issue audit + +Generated 2026-07-30 from a fresh scan of the **110** issues still open in [`TanStack/table`](https://github.com/TanStack/table/issues), cross-referenced against the two 2026-07-26 audits (`issues-gpt.md`, `issues-claude.md`) and re-verified against current v9 `alpha` source (post module-level-fns perf refactor; `9.0.0-beta.61` era, which includes PR [#6458](https://github.com/TanStack/table/pull/6458) merged 2026-07-29). + +## Result + +- **61 issues can close now without a new PR** — including 2 newly fixed by #6458 and 2 duplicates of a canonical selection issue. +- **49 issues should remain open:** 10 verified & urgent, 37 valid, 2 needing investigation. +- **115 of the 225 issues from the July 26 audits were closed between then and today; zero new issues have been filed since.** Every currently open issue was covered by both prior audits. +- Where the two audits disagreed (27 issues), each was re-adjudicated against current source: 9 resolved to close, 18 to keep open. +- **17 already-open PRs target keep-open issues** (see table below); landing or porting them to `alpha` would clear roughly a third of the keep-open list. + +## Audit baseline + +- **v9 baseline:** `9.0.0-beta.61` (source branch includes the module-level-fns perf refactor) +- **Method:** 9 parallel investigation agents; per-issue checks of issue threads (activity after 2026-07-26), current `packages/table-core/src` source, and open/merged PRs. No fixes applied, no PRs opened, no repo files modified. +- **Verdict rule:** disagreements between the prior audits were resolved by re-reading current source; "Verified & Urgent" means the defect was re-confirmed live in current code on a default or documented path. + +## Summary + +| Category | Count | Recommended action | +| --------------------------- | ------: | ------------------------------------------------------------------------- | +| Was fixed in V9 | 14 | Close; behavior is correct in current alpha (2 fixed by #6458 this week). | +| Wrong Library Direction | 32 | Close with the documented/headless-library boundary. | +| Junk Issue | 11 | Close as support-only, malformed, user error, or unreproducible-v8-era. | +| Couldn't Reproduce | 2 | Close unless a current minimal reproduction is supplied. | +| Duplicate | 2 | Close as duplicates of canonical #6049. | +| Verified & Urgent | 10 | Keep open and prioritize. | +| Valid | 37 | Keep open; many have fix PRs waiting to land or port. | +| Maybe & Needs Investigation | 2 | Keep only for additional triage. | +| **Total** | **110** | **61 close candidates; 49 keep open.** | + +## Fix-first priority list + +Ranked by user impact. Fixing the top four clusters closes or unblocks ~20 issues. + +1. **#6078 — `process is not defined` crash** (Verified & Urgent, high). Raw `process.env.NODE_ENV` reads survive into the published `dist` (`packages/table-core/dist/utils.js`) because `tsdown.config.ts` has no `define`; any bundler-less consumer (importmap/CDN) gets a hard `ReferenceError`, and v9 dropped the v8 UMD escape hatch. PR #6185 targets `main` only — needs an alpha port. +2. **Global-filter eligibility cluster — canonical #4673** (+ #4783, #4919, #5138). Default `getColumnCanGlobalFilter` still samples only `flatRows[0]` and requires string/number, and the AND in `column_getCanGlobalFilter` means explicit `enableGlobalFilter: true` can never opt back in — search silently returns nothing. Pending PRs: #6252/#6438 (nullish scan), #6439 (explicit opt-in). One combined fix closes three issues as duplicates. +3. **Parent/child selection reconciliation — canonical #6049** (+ #5116, #5398; close #4878/#5416 as duplicates). Deselecting a descendant never removes the ancestor id, so `getIsSelected()` and `getIsSomeSelected()` can both be true and the shipped expanding example renders a parent fully checked with an unselected child. One refactor of `table_toggleAllRowsSelected`/`mutateRowIsSelected` plausibly fixes five open issues plus the closed-set #4349 family. +4. **Auto-reset first-run guard — #5968.** v9 dropped v8's `registered` first-run guard: `memo` fires `onAfterUpdate` on the first computation, so the very first `getRowModel()` schedules `table_autoResetExpanded`/`table_autoResetPageIndex`, wiping controlled `expanded` and any initial `pageIndex` on mount (and `table_resetPageIndex(table, true)` hard-codes 0, ignoring `initialState`). +5. **`row._valuesCache` never invalidated — #4485 / #5363** (high). `createCoreRowModel` memoizes on `[options.data]` only, so a new `columns`/`accessorFn` silently serves stale values into rendering, sorting, and filtering — the documented "memoize columns" pattern yields wrong data. v8 PR #5582 shows the approach. +6. **#6007 — `inNumberRange` matches `null` as 0** (Verified & Urgent, high). No `resolveDataValue`, so `null`/`''`/`false` coerce to 0 and pass any zero-spanning range; this is the auto-selected default filter for number columns. PR #6313 open. +7. **#5770 — group-column visibility** (Valid, high). Explicit `columnVisibility: { groupId: false }` is ignored; Kevin already approved fixing it in the alpha ("Only in the alpha. Want to limit breaking changes in main.") and the alpha is the only window for this breaking change. + +## Open PRs to land or port + +| PR | Fixes | Status | +| ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------- | +| [#6439](https://github.com/TanStack/table/pull/6439) | #4673/#5138 (explicit `enableGlobalFilter` opt-in) | Open | +| [#6252](https://github.com/TanStack/table/pull/6252) / [#6438](https://github.com/TanStack/table/pull/6438) | #4783/#4919 (scan past nullish first row) | Open | +| [#6431](https://github.com/TanStack/table/pull/6431) | #4309 (single-sort replace branch) | Open | +| [#6313](https://github.com/TanStack/table/pull/6313) | #6007 (exclude non-numeric from `inNumberRange`) | Open | +| [#6361](https://github.com/TanStack/table/pull/6361) | #5987 (`maxLeafRowFilterDepth` flatRows) | Open | +| [#6417](https://github.com/TanStack/table/pull/6417) | #6081 (refresh custom faceted values) | Open | +| [#6443](https://github.com/TanStack/table/pull/6443) | #5801 (reset expansion on data change) | Open (base `beta`) | +| [#5790](https://github.com/TanStack/table/pull/5790) / [#6177](https://github.com/TanStack/table/pull/6177) | #5173 (`isSubRowSelected` with unselectable children) | Open | +| [#6075](https://github.com/TanStack/table/pull/6075) | #6074 (`columnFiltersMeta` wipe) | Open, targets v8 `main` — needs alpha port | +| [#5823](https://github.com/TanStack/table/pull/5823) | #5822 (pinned-row lookup crash) | Open since 2024-12, targets v8 — needs alpha port | +| [#6185](https://github.com/TanStack/table/pull/6185) | #6078 (`process.env` in dist) | Open, targets v8 — needs alpha port | +| [#6186](https://github.com/TanStack/table/pull/6186) | #6077 (Vue empty-string hydration) | Open, targets v8 — needs alpha port | +| [#6116](https://github.com/TanStack/table/pull/6116) | #6115 (expand-all writes non-expandable ids) | Open, targets v8 — needs alpha port | +| [#6184](https://github.com/TanStack/table/pull/6184) | #6136 (no-op toggleExpanded rerender) | Open, targets v8 — needs alpha port | +| [#6176](https://github.com/TanStack/table/pull/6176) | #6175 (example `server.allowedHosts`) | Open | +| [#5031](https://github.com/TanStack/table/pull/5031) | #4703-adjacent pageIndex clamp | Open, targets v8 | +| [#5582](https://github.com/TanStack/table/pull/5582) | #4485/#5363 (`_valuesCache` invalidation approach) | Open, targets v8 — port concept to alpha | + +## Close now + +### Was fixed in V9 (14) + +- [#4634 — "Cannot update a component while rendering a different component" when filtering with downshift combobox](https://github.com/TanStack/table/issues/4634) — The warning's source (v8's `useState`-backed `onStateChange` calling setState during render) no longer exists; v9 holds state in Store atoms, and PR #6458 (merged 2026-07-29) defers render-phase state publication to the commit phase, eliminating this warning class. +- [#4759 — Cannot toggle selection of grouped row to deselect its children](https://github.com/TanStack/table/issues/4759) — v9's `row_toggleSelected` dropped v8's early return and `mutateRowIsSelected` recurses into subRows while only writing selectable ids, so an unselectable group row now toggles its children on and off as requested. +- [#4794 — unnecessary rerenders of every row and cell in examples/react/editable-data](https://github.com/TanStack/table/issues/4794) — The targeted example no longer exists and the v8 root cause (all state in one `useState`) is gone: v9 state is atom-backed with `useTable` selectors and `table.Subscribe`, demonstrated in the spreadsheet example. The live `flexRender`-remount subtopic from recent comments deserves a fresh v9-scoped issue when closing. +- [#4879 — row selection not working for grouped rows (flat data)](https://github.com/TanStack/table/issues/4879) — Group rows never enter `rowSelection` in v9 (toggle-all walks pre-grouped flat rows; `mutateRowIsSelected` skips unselectable rows), and the example checkbox pattern adopted in #6364 renders both reported flows correctly. +- [#5026 — Can't perform a React state update on a component that hasn't mounted yet](https://github.com/TanStack/table/issues/5026) — Kevin's 2026-07-27 comment attributed this to controlled `options.state` syncing during render; PR #6458 (merged 2026-07-29) landed exactly that fix, deferring publication of controlled state to the commit phase. v8 will not be patched. +- [#5162 — custom Data within expanding example Crashing browser](https://github.com/TanStack/table/issues/5162) — The v8 crash was the unstable-`data` auto-reset-expanded render loop; in v9 `table_autoResetExpanded` fires only from the grouped row model and pagination resets are no-op-guarded, so the loop cannot occur. +- [#5605 — cell.getIsAggregated should not check for zero length subrows](https://github.com/TanStack/table/issues/5605) — `cell_getIsAggregated` now keys off `row.groupingColumnId` plus a resolved aggregation fn with no `subRows.length` term, exactly the requested behavior. +- [#5617 — Unexpected behavior of array filterFns (arrIncludes, arrIncludesAll, arrIncludesSome)](https://github.com/TanStack/table/issues/5617) — `filterFn_arrIncludesAll`/`arrIncludesSome` now bail on `!Array.isArray(dataValue)`, and the new `filterFn_arrHas` covers the scalar-in-filter-array semantic the reporter proposed. +- [#5620 — Table v8 expanding - incorrect state of parent checkbox when selecting subrows](https://github.com/TanStack/table/issues/5620) — The v8 `toggleSelected` early return that skipped clearing children is gone; deselecting a parent now clears its subrows. The remaining half is the documented indeterminate-state pattern. +- [#5836 — Request: Implement Uncontrolled State Behavior for Expanded Rows](https://github.com/TanStack/table/issues/5836) — v9's atomic state model delivers observe-without-controlling: leave `onExpandedChange` alone and subscribe to `table.atoms.expanded` (or use an external atom), so the table keeps owning state while the app gets notifications. +- [#5882 — alpha: getting the memo() function when calling `column.getFacetedUniqueValues()`](https://github.com/TanStack/table/issues/5882) — Fixed during alpha; `column_getFacetedUniqueValues` now returns the invoked `Map`, and the Qwik context it was reported from no longer exists in v9. +- [#6158 — Event `onPaginationChange` is triggered on every history push in RSC](https://github.com/TanStack/table/issues/6158) — Real in v8 (auto-reset called `setPageIndex(0)` with no equality check, producing the URL-sync loop); v9's `table_resetPageIndex` returns early when `newPageIndex === currentPageIndex`. +- [#6224 — alpha: `useTable` causes "Cannot update a component (SubscribeBound) while rendering" after setOptions moved to render body](https://github.com/TanStack/table/issues/6224) — Fixed by PR #6458 (merged 2026-07-29, in `9.0.0-beta.61`): `useTable` now calls `table_setOptions(..., { syncExternalState: false })` in the render body and publishes captured controlled state in a layout effect via `table_publishExternalState`, so no store subscriber is notified during render. Ask the reporter to confirm on beta.61. +- [#6450 — "Cannot update a component while rendering a different component" while using controlled expanded state](https://github.com/TanStack/table/issues/6450) — Duplicate root cause of #6224; PR #6458's summary explicitly cites this issue as the report it fixes. Close as fixed, asking for confirmation on beta.61. + +### Wrong Library Direction (32) + +- [#4498 — \[React\] Row selection does not reset after upgrading from v7 to v8](https://github.com/TanStack/table/issues/4498) — Row selection is id-keyed and intentionally persists across data changes; `resetRowSelection()`, `toggleAllRowsSelected(false)`, and stable `getRowId` are the supported answers, already in the thread. +- [#4628 — When sorting on date grouped column of year is not taken into account](https://github.com/TanStack/table/issues/4628) — Group rows sort by the column's aggregated value, and default `aggregationFn: 'auto'` resolves to `sum` for numbers, so summed timestamps look random; `aggregationFn: 'min'`/`'max'` on the date column yields the expected order (test-verified). Intentional pipeline semantics with a config fix. +- [#4771 — getIsAllRowsSelected returns unexpected result when using manual pagination](https://github.com/TanStack/table/issues/4771) — `getIsAllRowsSelected` is defined over the loaded rows; the page-aware APIs (`getIsAllPageRowsSelected`/`getToggleAllPageRowsSelectedHandler`) are the documented answer. +- [#4781 — Pagination and Row Selection](https://github.com/TanStack/table/issues/4781) — Server-side "select all" cannot materialize ids the client never received; v9 also added `toggleAllRowsSelected(false, { deselectAll: true })` for clearing unloaded pages. +- [#4785 — rowSpan doesn't work with useFlexLayout and useResizeColumns](https://github.com/TanStack/table/issues/4785) — Targets v7's removed plugin hooks; flex/absolute layouts fundamentally cannot preserve native rowSpan semantics, and v7 is frozen. +- [#4797 — Manual Pagination doesnt reset PageIndex when Manual Column Filtering. (server-side, v8)](https://github.com/TanStack/table/issues/4797) — With manual pagination the table cannot distinguish a filter change from a page fetch; the docs explicitly transfer reset policy to the app's filter handler. +- [#4812 — Row selection acting strange when using fully controlled table](https://github.com/TanStack/table/issues/4812) — Under manual pagination `row.id` defaults to `row.index`, so ids repeat per page; a stable `getRowId` is the documented requirement and resolved the report. +- [#4848 — incorrect Row ID in "Pagination Controlled" react example](https://github.com/TanStack/table/issues/4848) — Same root story as #4812; index-default row ids are by design and the targeted v8 example no longer exists in v9. +- [#4872 — Use columnOrder and initialize it, then the group column is always at the end](https://github.com/TanStack/table/issues/4872) — `columnOrder` orders leaf column ids by design; seed with `getAllLeafColumns().map(c => c.id)`. Optional docs clarification only. +- [#4880 — columnSizing state gets out of sync with actual column sizes when used with a full width table](https://github.com/TanStack/table/issues/4880) — `columnSizing` stores logical widths and table-core never reads DOM geometry; a `width: 100%` table's browser-distributed widths will always diverge. The actionable ask underneath is #4825 (flex-redistribution resize) — close into it. +- [#4905 — Filtered row disappears if its cell value was changed](https://github.com/TanStack/table/issues/4905) — Filtered row models are pure derivations of data plus filter state; a row edited out of an active filter correctly disappears (reporter concedes as much). +- [#4906 — Column pinning inconsistence](https://github.com/TanStack/table/issues/4906) — Group-column `getIsPinned` is a documented column-level summary; the per-region header-group APIs are the correct rendering signal, which the reporter found. +- [#4970 — Expanded rows become hidden within the table](https://github.com/TanStack/table/issues/4970) — Default `paginateExpandedRows: true` paginates sub-rows alongside parents; `paginateExpandedRows: false` is the documented option and resolved it for two commenters. +- [#4994 — Incorrect pagination behavior with autoResetPageIndex set to true when deleting rows](https://github.com/TanStack/table/issues/4994) — Despite the title, the reporter runs `autoResetPageIndex: false` and expects the table to still adjust `pageIndex` when page count shrinks; the pagination guide documents that opting out means handling resets yourself. The adjacent real clamp gap is tracked by #4703 and PR #5031. +- [#5120 — Fixed width is not applying for fix width column along with Full width table, Column Resizing and Column visibility.](https://github.com/TanStack/table/issues/5120) — `column.getSize()` is correct; a `w-full` table with `table-layout: auto` treats widths as hints. Use `table-layout: fixed` with `getSize()` widths. +- [#5305 — Vue Query's `onPaginationChange` has incorrect union type/unexpected behavior](https://github.com/TanStack/table/issues/5305) — `Updater = T | ((old: T) => T)` is the library-wide state contract; the correct narrowing pattern is shown in the v9 Vue table-state guide and in-thread. +- [#5405 — Unable to access Map entries with array keys by value in getFacetedUniqueValues function](https://github.com/TanStack/table/issues/5405) — Identity-keyed Map entries for array values are the documented default; the `getUniqueValues` column option exists for exactly this case and is documented in the v9 faceting guide. +- [#5455 — `useQwikTable(options)` causes vite to crash in dev mode](https://github.com/TanStack/table/issues/5455) — The Qwik adapter could not survive Qwik's serialization model and was removed from the repo (commit `620590e58`); there is no Qwik surface in v9 to fix. +- [#5500 — sortingFn: 'alphanumeric' returns numbers last](https://github.com/TanStack/table/issues/5500) — Numbers-after-letters is an intentional branch in the chunked comparator, preserved to avoid silently reordering existing tables; digits-first is a two-line custom sort fn shown in the issue. +- [#5536 — useQwikTable undefined sorting handlers \[QwikCity\]](https://github.com/TanStack/table/issues/5536) — Qwik's QRL serialization boundary dropped the method-bearing instance; the Qwik adapter was removed in v9 (same territory as #5455). +- [#5594 — Grouping by not using subRows](https://github.com/TanStack/table/issues/5594) — Grouping deliberately operates on the current level and leaves a `getSubRows` hierarchy intact; v9's pluggable row-model factories make a recursive groupBy a supported userland customization. +- [#5710 — when i useing thead Grouping. only set subHeader enableColumnFilter work, parent Column didn't work](https://github.com/TanStack/table/issues/5710) — Filtering applies to leaf columns with an accessor; a group column has no row value to filter, confirmed by the maintainer in-thread (2024-08-15). +- [#5792 — \[Feature\] Keep sorting order after changing row information](https://github.com/TanStack/table/issues/5792) — Row models are pure derivations with no mutation tracking; `manualSorting` or row pinning give full ordering control in userland. +- [#5800 — Function getIsSomeSelected() return false when has one child](https://github.com/TanStack/table/issues/5800) — Documented as "some but not all": with one child selected, all sub-rows are selected, so `false` is correct and `getIsAllSubRowsSelected()` returns `true`. +- [#5870 — Column resizing doesn't respect `clientWidth`, bad experience with limited width tables](https://github.com/TanStack/table/issues/5870) — Core resize math is absolute-pixel correct; the runaway values came from the v8 percentage-based example (not in v9), and clamping to rendered `clientWidth` requires DOM measurement a headless core deliberately does not do. +- [#5935 — New Rows bring from infinite scroll are coming with Expand = False if I collapse any row](https://github.com/TanStack/table/issues/5935) — `ExpandedState` is either `true` or an explicit id map by design; the supported escape hatch for defaulting newly-loaded rows to expanded is the documented `getIsRowExpanded` option. +- [#6010 — From Docs Full width Resizable example - on resizing a particular column its effecting columns previous to it](https://github.com/TanStack/table/issues/6010) — The handler writes only the dragged column's size; the neighbor shift is browser auto table-layout on a `width: 100%` table. A spreadsheet-style algorithm is a rendering strategy the consumer owns (or lands via #4825). +- [#6061 — sortUndefined and Excel like sorting](https://github.com/TanStack/table/issues/6061) — `sortUndefined` matches literal `undefined` only, by documentation and implementation; normalizing `null`/`''` in an `accessorFn` is the intended pattern, already posted in-thread. +- [#6091 — \[Docs\] Document how to do pivoting](https://github.com/TanStack/table/issues/6091) — Pivoting was a v6/v7 plugin removed in v8; there is no inconsistent doc to fix and no built-in pivot API planned. Grouping + aggregation + userland column generation is the answer. +- [#6102 — \[v7+\] Collapse header group](https://github.com/TanStack/table/issues/6102) — Feature request against EOL v7 for UI-affordance state the library deliberately does not own; buildable from `columnVisibility` plus a placeholder column. +- [#6109 — Incorrect TanStack Table behavior when using virtualized rows inside hidden tabs](https://github.com/TanStack/table/issues/6109) — The reporter's own stack trace and proposed guard live in `@tanstack/react-virtual`'s measurement of a `display: none` container; refile at TanStack/virtual. +- [#6243 — Chrome renderer freeze when TanStack row model gates a Suspense child](https://github.com/TanStack/table/issues/6243) — Table's role is supplying a synchronous `rows.length`; the freeze mechanism is a thrown timer-backed promise under Suspense in a Chrome production preview, and no row-model path can wedge a renderer process. Point to stable `data` refs and redirect to facebook/react or Chromium. + +### Junk Issue (11) + +- [#4901 — Table row count change causes scrolling on Chrome when table is height limited](https://github.com/TanStack/table/issues/4901) — Pure CSS/Radix layout problem in the reporter's shadcn markup; both in-thread fixes confirmed working by follow-up commenters. +- [#4917 — Manual Pagination doesn't update `table.getRowModel().rows` - it shows all results](https://github.com/TanStack/table/issues/4917) — User error answered three times: `manualPagination: true` declares that `data` is already the current page. +- [#4929 — Infinite loop caused by combining virtual scroll and grouping](https://github.com/TanStack/table/issues/4929) — Root-caused in-thread to unstable references (`data: data || []` per render) triggering the documented auto-reset loop; not a grouping/virtualization interaction. +- [#4942 — Cell's filterFn never gets called](https://github.com/TanStack/table/issues/4942) — Column `filterFn` runs for `columnFilters`; global filtering deliberately resolves the table-level `globalFilterFn`. The v9 guide now states this explicitly. +- [#5033 — VSCode Intellisense is very slow with @tanstack/react-table 8.9.2](https://github.com/TanStack/table/issues/5033) — Repro field is literally `*`; the reporter's own update attributes the slowdown to the surrounding file's other imports. v9 fully rewrote the type graph; invite a refile with a reproducible v9 project. +- [#5045 — Order + filter with formatted data](https://github.com/TanStack/table/issues/5045) — Support question with a dead sandbox; raw accessor + `cell` formatter + custom per-column `filterFn`/`sortingFn` already covers it. +- [#5047 — react-beautiful-dnd does not work with react-table v8](https://github.com/TanStack/table/issues/5047) — Works locally per the reporter, fails only in the sandbox, filed upstream at hello-pangea/dnd#661 and abandoned; Table owns no drag/drop layout. +- [#5093 — Dropdown in header fires the sorting fn.](https://github.com/TanStack/table/issues/5093) — Consumer-side event bubbling from a sort toggle attached to the `
`; `e.stopPropagation()` stands and the follow-up supplied no repro. +- [#5352 — When using virtual tables, high memory usage has always been unable to be reduced.](https://github.com/TanStack/table/issues/5352) — Feb 2024 v8 solid-table heap screenshots from a third-party Storybook demo; no minimal repro, zero comments in 2.5 years, and v9's prototype-based rows rewrote the allocation profile being measured. Ask for a v9 repro with measurements. +- [#6018 — Can react-table v7 be safely used with React 19?](https://github.com/TanStack/table/issues/6018) — Support question about an EOL major (last release 2022); the supported path is v8/v9. +- [#6024 — Performance Issue on table!](https://github.com/TanStack/table/issues/6024) — Vague 3,000×3,000-cell v8 report with no profile; pagination/virtualization is the answer and nothing is actionable without a fresh v9 repro. + +### Couldn't Reproduce (2) + +- [#4852 — Resizing a column makes other columns resized when changing the size property](https://github.com/TanStack/table/issues/4852) — A core test shows resizing a leaf writes exactly one `columnSizing` entry; residual visual shifting is native full-width CSS layout (conceptually tracked under #4825). +- [#6048 — enableSortingRemoval doesnt seem to be working with manualSorting](https://github.com/TanStack/table/issues/6048) — The toggle cycle never consults `manualSorting` and a 4-case matrix test cycles removal correctly; neither the reporter nor the Feb 2026 confirmer supplied a repro. The plausible real mechanism (auto-sort-dir flip on server data swap) is tracked by #5147/#5832. + +### Duplicates (2) + +- [#4878 — row selection not working for nested rows (`getSubRows`)](https://github.com/TanStack/table/issues/4878) — Half fixed (the #6364 example checkbox pattern covers select-all-children); the remaining half (deselecting a child leaves the parent id in `rowSelection`) is exactly the root cause tracked by canonical #6049. Close with a pointer. +- [#5416 — Selection of subrows not working correctly](https://github.com/TanStack/table/issues/5416) — Both symptoms reduce to the missing upward reconciliation in `mutateRowIsSelected`; identical root cause to #6049, which states the scenarios more precisely. + +## Keep open + +### Verified & Urgent (10) + +- [#4673 — Global filter doesn't run when accessor key points to an object](https://github.com/TanStack/table/issues/4673) — **high; canonical for the global-filter cluster.** Default `getColumnCanGlobalFilter` samples only `flatRows[0]` and requires string/number (`globalFilteringFeature.ts:30-37`), and the AND in `column_getCanGlobalFilter` means explicit `enableGlobalFilter: true` can never override — a custom `globalFilterFn` is silently skipped. PR #6439 open. This issue exercises both facets (nullish scan + opt-in), so keep it as canonical. +- [#4783 — Global Filter Fails When First Row Value is Undefined](https://github.com/TanStack/table/issues/4783) — **high.** Same first-row-sampling defect: a column whose row-0 value is `undefined`/`null` is silently excluded from global filtering, returning empty search results on the default path. PRs #6252/#6438 open. Closeable as duplicate of #4673 once one fix merges. +- [#4919 — Filter not working if accessorFn returns null for any row](https://github.com/TanStack/table/issues/4919) — **high.** The column-filter half is fixed in v9 (`column_getAutoFilterFn` scans past nullish), but the global-filter half persists. Same cluster and pending PRs as #4783. +- [#5173 — row.getIsSomeSelected returning true for row with disabled subrows](https://github.com/TanStack/table/issues/5173) — **medium.** `isSubRowSelected` (`rowSelectionFeature.utils.ts:909-941`) still initializes `allChildrenSelected = true` and never clears it when all children are non-selectable, returning `'all'` with zero selected rows and propagating bogus `someSelected` upward. Two open fix PRs (#5790, #6177) — land one on alpha. +- [#5363 — `getValue` cache not invalidating when `accessorFn` is updated](https://github.com/TanStack/table/issues/5363) — **high.** `row_getValue` serves `row._valuesCache` forever and `createCoreRowModel` memoizes on `[options.data]` only, so swapping columns/`accessorFn` silently feeds stale values into rendering, sorting, and filtering. v8 PR #5582 shows the fix approach; same root cause as #4485/#4702. +- [#5822 — Pinning aggregation rows then ungrouping causes a hard error](https://github.com/TanStack/table/issues/5822) — **medium.** `table_getPinnedRows` still calls the throwing `table.getRow(rowId, true)` unguarded (`rowPinningFeature.utils.ts:126`), so a pinned synthetic group id crashes `getTopRows()`/`getBottomRows()` after ungrouping. Reporter's fix PR #5823 open and unreviewed since 2024-12. +- [#5968 — Invoking `table.getRowModel()` causes rows to collapse](https://github.com/TanStack/table/issues/5968) — **high.** v9 dropped v8's first-run auto-reset guard: `memo` fires `onAfterUpdate` on the first computation and `createGroupedRowModel` forces `rowInputsChanged = true` on that first run, so the first `getRowModel()` schedules `table_autoResetExpanded`/`table_autoResetPageIndex` — wiping controlled `expanded` and any initial `pageIndex` on mount (`table_resetPageIndex(table, true)` hard-codes 0, ignoring `initialState`). Canonical survivor of the #5970/#6025/#6030 cluster (all since closed). +- [#6007 — When using filterVariant: 'range' and filtering with a min value of 0, null values are also included in the results](https://github.com/TanStack/table/issues/6007) — **high.** `filterFn_inNumberRange` has no `resolveDataValue`, so `null`/`''`/`false` coerce to 0 and match any zero-spanning range — and it is the auto-selected default filter for number columns. Sibling `inDateRange` already does this correctly. PR #6313 open. +- [#6049 — Row Selection: Incorrect Parent Row Selection Behavior with Nested Data](https://github.com/TanStack/table/issues/6049) — **high; canonical for the selection-reconciliation cluster.** Deselecting descendants never removes the ancestor id and `row_getIsSelected` reads only its own id, so a parent can report `getIsSelected()` and `getIsSomeSelected()` true simultaneously; the shipped expanding example renders it fully checked with an unselected child, and `getSelectedRowModel()` keeps the stale parent. Close #4878/#5416 into this. +- [#6078 — `process is not defined` when used in Vanilla JS (without Node.js)](https://github.com/TanStack/table/issues/6078) — **high.** Raw `process.env.NODE_ENV` reads persist in source (~14 sites) and survive into the published build (`packages/table-core/dist/utils.js`) because `tsdown.config.ts` has no `define`; any bundler-less consumer gets a hard `ReferenceError`, and v9 dropped the v8 UMD escape hatch. PR #6185 targets v8 `main` only — needs an alpha fix. + +### Valid (37) + +**High priority** + +- [#4485 — React: accessor function don't trigger when data is outside of exported component](https://github.com/TanStack/table/issues/4485) — Same `_valuesCache` defect as #5363, kept for the columns-swap case: a brand-new `columns` array with a new `accessorFn` still serves stale values, so the documented "memoize columns on a dependency" pattern silently yields wrong data. One invalidation change fixes both. +- [#5770 — Parent columns in groups are not respecting the isVisible value we're passing in](https://github.com/TanStack/table/issues/5770) — `column_getIsVisible` short-circuits on `column.columns.length` and returns `childColumns.some(getIsVisible)`, so an explicit `columnVisibility: { groupId: false }` is ignored whenever any child is visible. Kevin approved the fix direction in-thread ("Only in the alpha. Want to limit breaking changes in main."); the alpha is the only window and nothing has landed. + +**Medium priority** + +- [#4309 — toggleSorting preserve the multi selection](https://github.com/TanStack/table/issues/4309) — In non-multi mode, `replace` is chosen only when the column is not the last sorted entry (`rowSortingFeature.utils.ts:253-262`), so clicking the last sorted column preserves all other sorts while clicking an earlier one replaces — same gesture, two behaviors. PR #6431 open. +- [#4825 — implement full width resize handler](https://github.com/TanStack/table/issues/4825) — The redistribute-delta-into-next-column mode still doesn't exist and is pure `ColumnSizingState` math (no DOM measurement), so the headless-boundary objection fails. Canonical tracking issue for #4880/#4852/#6010-family demand. +- [#4946 — enableMultiRemove attribute is invalid](https://github.com/TanStack/table/issues/4946) — `column_toggleSorting` calls `column_getNextSortingOrder(column)` with no `multi` argument, so the documented option is dead on every path including the recommended handler. +- [#5008 — Median aggregation fails if null values](https://github.com/TanStack/table/issues/5008) — `aggregationFn_median` bails to `undefined` on the first non-number while `mean`/`sum`/`min`/`max` skip nullish values; now documented as-is, making it intentional-by-inertia rather than by design. +- [#5116 — getToggleAllRowsSelectedHandler doesn't take into account enableSubRowSelection property](https://github.com/TanStack/table/issues/5116) — Four select-all paths disagree: `table_toggleAllRowsSelected` ignores `row_getCanSelectSubRows`, the page variant honors it, and `table_getIsAllPageRowsSelected` still requires sub-rows selected, so the page header checkbox can never render checked with `enableSubRowSelection: false`. Contradicts docs; distinct from the #6049 cluster. +- [#5138 — column.enableGlobalFilter is not considered in the default implemenation of table.getColumnCanGlobalFilter](https://github.com/TanStack/table/issues/5138) — The opt-in half of the #4673 cluster; PR #6439 targets exactly this. Workaround exists (custom `getColumnCanGlobalFilter`), hence medium; closeable as duplicate once the fix lands. +- [#5147 — Sorting direction not updating properly with manual sorting and same values on current page](https://github.com/TanStack/table/issues/5147) — `column_getAutoSortDir` derives direction from `flatRows[0]` only, so a manual-sorting server data swap flips the toggle cycle and silently drops a state. `sortDescFirst` is the documented workaround; shares root cause with #5832. +- [#5202 — The header object has the 'rowSpan' property, but there is an issue where it is not applied](https://github.com/TanStack/table/issues/5202) — `Header.rowSpan` is 0 for every header in every table shape (`buildHeaderGroups.ts` `updateHeaderSpans`); dead public API that should be implemented or removed before v9 stable. +- [#5397 — there is a bug when you try sticky pin column group](https://github.com/TanStack/table/issues/5397) — `table_getColumnOffsets` builds offsets from visible leaf columns only, so `column_getStart`/`getAfter` fall through to `?? 0` for group columns and pinned group headers stack at offset 0. `header.getStart()` is correct, giving a workaround. +- [#5398 — Toggle select all rows doesn't respect `canSelect()` condition](https://github.com/TanStack/table/issues/5398) — Deselect branches unconditionally delete while select branches guard on `row_getCanSelect`; a commenter documents the guard existed in PR #5061 commit b76bb8d and was dropped in 42f7595 — a regression, not a design decision. +- [#5497 — Column visibility APIs do not work with column groups](https://github.com/TanStack/table/issues/5497) — `column_toggleVisibility` writes a dead state key for group ids while `column_getIsVisible` derives from children, and `column_getCanHide` advertises hideability it cannot deliver. +- [#5653 — Custom sortingFn not being run when row key is undefined](https://github.com/TanStack/table/issues/5653) — The default `sortUndefined: 1` short-circuits before invoking the user's sortFn for undefined pairs, and `docs/framework/react/guide/sorting.md:430` describes `false` incorrectly ("considered tied" vs actually passing undefined through). At minimum a docs defect plus a legitimate pass-through-mode request. +- [#5778 — ColumnGrouping.getDefaultColumnDef should not return aggregatedCell](https://github.com/TanStack/table/issues/5778) — The aggregation feature unconditionally injects a default `aggregatedCell` and FlexRender resolves `aggregatedCell ?? cell`, so a user's `cell` formatter can never win on aggregated cells without the `aggregatedCell: null` workaround. Small breaking change that belongs in the alpha. +- [#5801 — autoResetExpanded not reset the expanded state on data change in V8](https://github.com/TanStack/table/issues/5801) — In v9, `table_autoResetExpanded` is wired only from the grouped row model, so expanding-without-grouping never auto-resets on data change. Fix PR #6443 (base `beta`) implements the audit's fix note; close via that PR when it lands. +- [#5832 — Unexpected sorting behavior arises due to the implementation in the @tanstack/table-core library](https://github.com/TanStack/table/issues/5832) — Same `column_getAutoSortDir` first-row sampling as #5147: a leading null flips the auto direction and the cycle silently drops a state. +- [#5833 — `flatRows` contains duplicate sub-rows when pagination is enabled](https://github.com/TanStack/table/issues/5833) — `_createPaginatedRowModel`'s walk pushes each row then recurses into `subRows`, but `rows` is already `expandRows()`-flattened, so expanded children land in `flatRows` twice. +- [#5908 — Wrong type for `GlobalFilterTableState`](https://github.com/TanStack/table/issues/5908) — `globalFilter: any` lets a nested `{ globalFilter }` object type-check via `satisfies` and silently misbehave, and the `any` poisons `TableState`. Widen to `unknown` or a threaded generic during the alpha. +- [#5971 — `Table` is not assignable to `Table`](https://github.com/TanStack/table/issues/5971) — Re-verified with a scratch tsc 6.0.3 compile: assignment still fails through `optionsStore` atom invariance into contravariant option callbacks. The variance programme already fixed Table→Table_Internal; finishing the job fits current maintainer direction. +- [#5987 — getFacetedRowModel inconsistently respects maxLeafRowFilterDepth: 0](https://github.com/TanStack/table/issues/5987) — `filterRowModelFromRoot` keeps parents' original `subRows` but never pushes those descendants into `flatRows`/`rowsById`, so facet counts under-count. PR #6361 open. +- [#6074 — filterFromLeafRows causes columnFiltersMeta to get wiped](https://github.com/TanStack/table/issues/6074) — `filterRowModelFromLeafs` copies only `columnFilters` when rebuilding rows, dropping `columnFiltersMeta` and breaking the fuzzy-filter + rank-sort recipe. One-line fix; PR #6075 (v8) needs an alpha port. +- [#6081 — Custom `getFacetedUniqueValues` does not update on rerenders](https://github.com/TanStack/table/issues/6081) — Two cache layers freeze server-side faceting results (per-column factory cache + `memoDeps: [facetedRowModel.flatRows]`), making the documented "Custom Server-Side Faceting" recipe non-functional. PR #6417 open. +- [#6101 — `filterFn.autoRemove` ignored for empty strings](https://github.com/TanStack/table/issues/6101) — `shouldAutoRemoveFilter` ORs the custom `autoRemove` result with a hardcoded empty-string check, so a custom `autoRemove` returning false for `''` can never win — filtering for empty strings is impossible. +- [#6115 — Expanding API including rows that dont include a subRow](https://github.com/TanStack/table/issues/6115) — `row_toggleExpanded` materializes `expanded === true` by writing every id from `rowsById` with no expandability filter, polluting state and skewing `getExpandedDepth`. PR #6116 (v8) needs an alpha port. +- [#6230 — HeaderContext and CellContext types should use ReactTable](https://github.com/TanStack/table/issues/6230) — `CellContext.table` is typed as core `Table` while the runtime object in React carries `Subscribe`/`FlexRender`, and docs push `table.Subscribe` inside cell renderers, so users hit type errors on real APIs. Kevin acknowledged the gap in-thread (2026-04-29); fix lives in the adapter via module augmentation, not core. + +**Low priority** + +- [#4512 — Column resizing on scaled transformation](https://github.com/TanStack/table/issues/4512) — `header_getResizeHandler` captures raw viewport `clientX` with no scale input, so resizing drifts by the factor k under `transform: scale(k)`. Needs an API decision (scale option or derived scale). +- [#4939 — setting `enableExpanding` has no effect.](https://github.com/TanStack/table/issues/4939) — `row_toggleExpanded` and `table_toggleAllRowsExpanded` never consult `row_getCanExpand`, unlike the selection twin which guards its imperative path. Cross-feature inconsistency, trivial fix; the documented handler path works. +- [#5019 — Solid Table: Examples from docs cause unnecessary rerenders of components when data updates](https://github.com/TanStack/table/issues/5019) — Every v9 Solid example still uses ``, which recreates all row DOM on data change, and the Solid guides contain no ``/``/`createStore` guidance. Docs plus one fine-grained example; the in-thread solution works. +- [#5275 — A table doesn't get updated when a defaultColumn changes](https://github.com/TanStack/table/issues/5275) — `constructColumn` bakes the default def in at construction while `getAllColumns` memoizes on `[options.columns]` only, yet `getDefaultColumnDef` itself re-memoizes on `[options.defaultColumn]` — the table reports the new default while columns keep the old one. Defect or docs gap; fix has a tradeoff around inline `defaultColumn` objects. +- [#5850 — Table: Row selection is not cleaned up when table data is removed.](https://github.com/TanStack/table/issues/5850) — By-design as filed, but the thread converged on `table.removeRowSelection(ids)` which Kevin explicitly agreed to add to the v9 alpha; no such helper exists yet. Keep as an accepted enhancement. +- [#5864 — Add `mode()` aggregation function to find most common unique value.](https://github.com/TanStack/table/issues/5864) — Tree-shakeable individual aggregation fns make this near-zero-cost; the reporter supplied a tested implementation. +- [#5909 — TableState does not account for options.state properties being set as undefined](https://github.com/TanStack/table/issues/5909) — `table_syncExternalStateToBaseAtoms` still writes `undefined` slices into base atoms with no guard (survives the #6458 refactor), violating the non-optional `TableState` type. Downstream code is defensive, so a type-contract violation rather than a visible failure. +- [#6077 — \[Vue\] Hydration error in `FlexRender` when column is an empty string](https://github.com/TanStack/table/issues/6077) — Vue `flexRender` returns string results verbatim, so an empty-string cell produces an SSR text-node mismatch. PR #6186 (v8) needs an alpha port. +- [#6136 — Calling row.toggleExpanded(bool) triggers re-render even without state change](https://github.com/TanStack/table/issues/6136) — `table_setExpanded` invokes `onExpandedChange` unconditionally even for no-op updates, so controlled-state consumers get spurious callbacks; the uncontrolled atom path already bails on identity. PR #6184 (v8) needs an alpha port. +- [#6175 — Unable to view examples in Codesandbox without forking](https://github.com/TanStack/table/issues/6175) — No example Vite config on `alpha` sets `server.allowedHosts`, so CodeSandbox devbox embeds are blocked by Vite's host check. PR #6176 open. +- [#6302 — Accessor key does not have the same typing as Form's name key](https://github.com/TanStack/table/issues/6302) — `accessorKey` is `(string & {}) | keyof TData` while `columnHelper.accessor` already constrains to `DeepKeys`; the library ships the requested type and simply doesn't use it on the raw ColumnDef form. Gated on a TS-perf check against wide `TData`. + +### Maybe & Needs Investigation (2) + +- [#5696 — Table using insane amount of memory even with virtualisation when scrolling](https://github.com/TanStack/table/issues/5696) — `row_getAllCells` still materializes a Cell per leaf column per row into `row._cellsCache` (now a WeakMap) retained for the lifetime of a stable columns reference, so column virtualization does not avoid rows×cols cell construction. Prior scratch estimate ~122 B/cell (~470 MB extrapolated at 2000×2000); needs a fresh browser heap profile separating table vs virtual vs renderer cost. +- [#6011 — virtual columns experimental can not scroll smoothly when nearing the end](https://github.com/TanStack/table/issues/6011) — The repro is this repo's own shipped `virtualized-columns-experimental` example, so even if the root cause is TanStack Virtual measurement, the fix is plausibly example-side (row virtualizer `measureElement` changing total height mid-drag). One cheap experiment (fixed row `estimateSize` or `overflow-y: scroll`) decides between fixing the example and transferring to the Virtual repo. + +## Root-cause clusters (updated from the July 26 audits) + +| # | Root cause | Open issues | Status | +| --- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------ | +| 1 | v9 dropped v8's first-run auto-reset guard; first `getRowModel()` wipes controlled/initial `expanded`/`pageIndex` | #5968 (canonical; #5970/#6025/#6030 already closed) | Unfixed; `table_resetPageIndex(table, true)` also ignores `initialState` | +| 2 | Global-filter eligibility decided from `flatRows[0]`; explicit `enableGlobalFilter: true` cannot override | #4673 (canonical), #4783, #4919, #5138 | PRs #6252/#6438/#6439 open | +| 3 | `row._valuesCache` never invalidated when columns/`accessorFn` change | #4485, #5363 (#4702 closed) | v8 PR #5582 open; port concept to alpha | +| 4 | Parent/child selection never reconciled upward | #6049 (canonical), #5116, #5398; close #4878/#5416 as dups | Unfixed; one refactor fixes the family | +| 5 | `column_getAutoSortDir` samples `flatRows[0]` | #5147, #5832 | Unfixed; `sortDescFirst` workaround | +| 6 | Non-multi sort toggle preserves other sorts on the last-sorted column | #4309 (#6070 closed) | PR #6431 open | +| 7 | Group (non-leaf) columns second-class in visibility/offsets | #5497, #5770, #5397 (#4872/#5577 closed as docs-direction) | Unfixed; #5770 has maintainer-approved direction | +| 8 | v8-targeted fix PRs never ported to alpha | #6074, #6077, #6078, #6115, #6136, #5822 | Six open PRs against `main` need alpha ports | + +## Changes vs the July 26 audits + +- **#6224 / #6450:** were Verified & Urgent in both audits; **fixed by PR #6458** (merged 2026-07-29) — `useTable` now defers controlled-state publication to a layout effect. Close both. +- **#5026 / #4634:** same fix family; the 2026-07-27 maintainer comment plus #6458 flips them from open questions to closeable. +- **#6078:** was Maybe/Valid; **escalated to Verified & Urgent** — the `process.env` reads demonstrably survive into the published v9 dist and there is no workaround. +- **#4994:** was Verified & Urgent in the GPT audit; **flips to close** — the reporter explicitly opted out with `autoResetPageIndex: false`, and the docs assign reset responsibility to the app; the real clamp gap is tracked by #4703 (closed) and PR #5031. +- **#4878 / #5416:** keep-verdicts consolidated into close-as-duplicate of canonical #6049. +- **#5850:** GPT's close verdict overturned — Kevin committed in-thread to adding `removeRowSelection(ids)` to the v9 alpha; the helper does not exist yet. From aef2916404aea3e35e1ad3cf5ab77bca50aaa1e6 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Fri, 31 Jul 2026 14:33:24 -0500 Subject: [PATCH 3/3] move to compiled tsrx --- _artifacts/domain_map.yaml | 6 +- _artifacts/skill_spec.md | 6 +- docs/framework/octane/quick-start.md | 2 +- .../reference/functions/createTableHook.md | 2 +- .../octane/reference/functions/useTable.md | 2 +- .../octane/reference/variables/Subscribe.md | 2 +- docs/installation.md | 2 +- packages/octane-table/package.json | 47 ++++++++---- .../octane-table/scripts/finalize-types.mjs | 69 ++++++++++++++++++ .../skills/create-table-hook/SKILL.md | 2 +- .../skills/getting-started/SKILL.md | 4 +- .../octane-table/skills/table-state/SKILL.md | 2 +- packages/octane-table/src/Subscribe.tsrx | 2 - packages/octane-table/src/Subscribe.tsrx.d.ts | 8 +-- .../octane-table/src/createTableHook.tsrx | 36 +++++----- .../src/createTableHook.tsrx.d.ts | 8 +-- packages/octane-table/src/index.d.ts | 8 --- packages/octane-table/src/index.ts | 3 - packages/octane-table/src/useTable.tsrx.d.ts | 8 +-- .../tests/conformance/parity.test.ts | 3 +- .../tests/type-tests/public-surface.tsx | 4 -- packages/octane-table/tsconfig.build.json | 13 ++++ packages/octane-table/tsconfig.json | 3 + packages/octane-table/tsdown.config.ts | 71 +++++++++++++++++++ scripts/generateDocs.js | 2 - 25 files changed, 233 insertions(+), 82 deletions(-) create mode 100644 packages/octane-table/scripts/finalize-types.mjs delete mode 100644 packages/octane-table/src/index.d.ts create mode 100644 packages/octane-table/tsconfig.build.json create mode 100644 packages/octane-table/tsdown.config.ts diff --git a/_artifacts/domain_map.yaml b/_artifacts/domain_map.yaml index 3ee4122471..fcfa93310b 100644 --- a/_artifacts/domain_map.yaml +++ b/_artifacts/domain_map.yaml @@ -111,8 +111,8 @@ api_discovery_policy: feature_source: 'node_modules/@tanstack/table-core/dist/features//' ember_entrypoint: 'node_modules/@tanstack/ember-table/declarations/index.d.ts' angular_entrypoint: 'node_modules/@tanstack/angular-table/dist/types/' - octane_entrypoint: 'node_modules/@tanstack/octane-table/src/index.ts and matching *.tsrx.d.ts sidecars' - fallback: 'If declarations are unavailable, resolve the installed package root and inspect published types; do not open package src/ except for source-distributed Octane, or substitute v8 / another adapter API.' + octane_entrypoint: 'node_modules/@tanstack/octane-table/dist/types/index.d.ts' + fallback: 'If declarations are unavailable, resolve the installed package root and inspect published types; do not open package src/ or substitute v8 / another adapter API.' coverage: ignored_packages: @@ -1292,7 +1292,7 @@ skills: domain: framework-adapters type: framework framework: octane - purpose: 'Create and render an Octane v9 table with authored TSRX, stable inputs, keyed lists, and component-scoped render helpers.' + purpose: 'Create and render an Octane v9 table with application-authored TSRX, stable inputs, keyed lists, and component-scoped render helpers.' sources: [ 'TanStack/table:docs/framework/octane/quick-start.md', diff --git a/_artifacts/skill_spec.md b/_artifacts/skill_spec.md index c4d9f2d96b..b08a0cfd69 100644 --- a/_artifacts/skill_spec.md +++ b/_artifacts/skill_spec.md @@ -47,11 +47,11 @@ Every skill that discusses APIs must tell the consuming agent how to inspect the - Stock feature API: node_modules/@tanstack/table-core/dist/features/FEATURE/. - Ember API: node_modules/@tanstack/ember-table/declarations/index.d.ts (and sibling `declarations/*.d.ts`). - Angular API: node_modules/@tanstack/angular-table/dist/types/\*.d.ts (bundled public API; do not expect `src/helpers/` under the published package). -- Octane API: node_modules/@tanstack/octane-table/src/index.ts, the matching `*.tsrx.d.ts` sidecar, and `src/types.ts` (the package intentionally distributes authored TSRX source). +- Octane API: node_modules/@tanstack/octane-table/dist/types/index.d.ts (and sibling `dist/types/*.d.ts`). - Devtools API: node_modules/@tanstack/FRAMEWORK-table-devtools/dist/index.d.ts or @tanstack/table-devtools/dist/index.d.ts. - Fuzzy ranking API: node_modules/@tanstack/match-sorter-utils/dist/index.d.ts. -Do not open package `src/` under `node_modules` unless the package intentionally publishes source, as `@tanstack/octane-table` does. Do not direct agents to a GitHub main-branch source file when an installed package is available. Installed declarations and published source keep guidance aligned with the consumer package version. +Do not open package `src/` under `node_modules` (it is not published). Do not direct agents to a GitHub main-branch source file when an installed package is available. Installed declarations keep guidance aligned with the consumer package version. ## Skill writing contract @@ -307,7 +307,7 @@ All Devtools skills must emphasize the required non-empty table options.key, lif ### Octane -- The package distributes authored TSRX; consumer tooling must compile it with Octane, and components use TSRX component bodies plus keyed `@for` loops where appropriate. +- The adapter ships compiled JavaScript and declarations. Consumer tooling compiles the application's TSRX components, which use TSRX component bodies plus keyed `@for` loops where appropriate. - `useTable` stages fresh options for same-render reads, selects `table.state`, and publishes controlled state only from an accepted layout commit; abandoned work cannot notify the store. - Render `table.Subscribe` and the createTableHook App wrappers as components so each has an independent Octane hook/context scope; never invoke them as plain functions. - Use `@tanstack/octane-store` for external atoms. External atoms are synchronous owners and take precedence over controlled `options.state`. diff --git a/docs/framework/octane/quick-start.md b/docs/framework/octane/quick-start.md index 7b4440b505..7d68e21897 100644 --- a/docs/framework/octane/quick-start.md +++ b/docs/framework/octane/quick-start.md @@ -95,7 +95,7 @@ A few things to note: - `tableFeatures({})` declares which optional features the table uses. Registering only what you need keeps bundles small and gives TypeScript accurate types for the table instance. - The core row model is always included automatically. Feature row models (sorting, filtering, pagination) are registered as slots directly on the `tableFeatures({...})` call when you need them. - `table.FlexRender` renders the `header`, `cell`, and `footer` definitions from your columns, whether they are plain values or Octane components. -- Octane compiles authored TSRX for the current client or server target. Keep `.tsrx` source in your application compilation path. +- Octane compiles your application's authored TSRX for the current client or server target. The Table adapter itself ships compiled JavaScript and declarations. See the full [Basic useTable example](./examples/basic-use-table) for a runnable version with more columns and a footer. diff --git a/docs/framework/octane/reference/functions/createTableHook.md b/docs/framework/octane/reference/functions/createTableHook.md index 93494318f7..6f6e96b4a9 100644 --- a/docs/framework/octane/reference/functions/createTableHook.md +++ b/docs/framework/octane/reference/functions/createTableHook.md @@ -9,7 +9,7 @@ title: createTableHook function createTableHook(options): CreateTableHookResult; ``` -Defined in: [createTableHook.tsrx.d.ts:13](https://github.com/TanStack/table/blob/main/packages/octane-table/src/createTableHook.tsrx.d.ts#L13) +Defined in: createTableHook.tsrx.d.ts:11 ## Type Parameters diff --git a/docs/framework/octane/reference/functions/useTable.md b/docs/framework/octane/reference/functions/useTable.md index 6e78b68355..ba486b91dd 100644 --- a/docs/framework/octane/reference/functions/useTable.md +++ b/docs/framework/octane/reference/functions/useTable.md @@ -9,7 +9,7 @@ title: useTable function useTable(tableOptions, selector?): OctaneTable; ``` -Defined in: [useTable.tsrx.d.ts:14](https://github.com/TanStack/table/blob/main/packages/octane-table/src/useTable.tsrx.d.ts#L14) +Defined in: useTable.tsrx.d.ts:12 ## Type Parameters diff --git a/docs/framework/octane/reference/variables/Subscribe.md b/docs/framework/octane/reference/variables/Subscribe.md index 69150fb07c..95d7cdc4b0 100644 --- a/docs/framework/octane/reference/variables/Subscribe.md +++ b/docs/framework/octane/reference/variables/Subscribe.md @@ -9,4 +9,4 @@ title: Subscribe const Subscribe: SubscribeComponent; ``` -Defined in: [Subscribe.tsrx.d.ts:8](https://github.com/TanStack/table/blob/main/packages/octane-table/src/Subscribe.tsrx.d.ts#L8) +Defined in: Subscribe.tsrx.d.ts:6 diff --git a/docs/installation.md b/docs/installation.md index 112d8fe017..6558c250e5 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -36,7 +36,7 @@ The `@tanstack/preact-table` package works with Preact 10 or newer. # Octane -The `@tanstack/octane-table` package works with Octane 0.1.21. It publishes authored TSRX, so your application must use Octane's compiler integration (for example, the Octane Vite plugin) to compile the package separately for its client and server targets. +The `@tanstack/octane-table` package works with Octane 0.1.21. Configure Octane's compiler integration (for example, `octane()` with Vite) for your application's TSRX components. # Vue diff --git a/packages/octane-table/package.json b/packages/octane-table/package.json index 23497a128e..fd6726c369 100644 --- a/packages/octane-table/package.json +++ b/packages/octane-table/package.json @@ -22,17 +22,34 @@ "tanstack-intent" ], "type": "module", - "main": "./src/index.ts", - "module": "./src/index.ts", - "types": "./src/index.d.ts", + "types": "./dist/types/index.d.ts", + "main": "./dist/server/index.js", + "module": "./dist/client/index.js", "exports": { ".": { - "types": "./src/index.d.ts", - "default": "./src/index.ts" + "types": "./dist/types/index.d.ts", + "browser": "./dist/client/index.js", + "node": "./dist/server/index.js", + "default": "./dist/client/index.js" + }, + "./experimental-worker-plugin": { + "types": "./dist/types/experimental-worker-plugin.d.ts", + "browser": "./dist/client/experimental-worker-plugin.js", + "node": "./dist/server/experimental-worker-plugin.js", + "default": "./dist/client/experimental-worker-plugin.js" + }, + "./flex-render": { + "types": "./dist/types/flex-render.d.ts", + "browser": "./dist/client/flex-render.js", + "node": "./dist/server/flex-render.js", + "default": "./dist/client/flex-render.js" + }, + "./static-functions": { + "types": "./dist/types/static-functions.d.ts", + "browser": "./dist/client/static-functions.js", + "node": "./dist/server/static-functions.js", + "default": "./dist/client/static-functions.js" }, - "./experimental-worker-plugin": "./src/experimental-worker-plugin.ts", - "./flex-render": "./src/flex-render.ts", - "./static-functions": "./src/static-functions.ts", "./package.json": "./package.json" }, "sideEffects": false, @@ -40,10 +57,16 @@ "node": ">=22" }, "files": [ - "src", - "skills", - "README.md" + "dist", + "skills" ], + "octane": { + "hookSlots": { + "manual": [ + "dist" + ] + } + }, "scripts": { "clean": "rimraf ./build && rimraf ./dist", "test:eslint": "eslint ./src --ext .ts", @@ -53,7 +76,7 @@ "test:lib:dev": "pnpm test:lib:client --watch", "test:types": "tsrx-tsc --noEmit", "test:build": "publint --strict", - "build": "tsrx-tsc --noEmit" + "build": "rimraf ./dist && tsdown && tsrx-tsc -p tsconfig.build.json && node scripts/finalize-types.mjs" }, "dependencies": { "@tanstack/octane-store": "^0.12.0", diff --git a/packages/octane-table/scripts/finalize-types.mjs b/packages/octane-table/scripts/finalize-types.mjs new file mode 100644 index 0000000000..2aba377ca1 --- /dev/null +++ b/packages/octane-table/scripts/finalize-types.mjs @@ -0,0 +1,69 @@ +import { + copyFileSync, + readFileSync, + readdirSync, + statSync, + writeFileSync, +} from 'node:fs' +import { join, resolve } from 'node:path' + +const packageRoot = resolve(import.meta.dirname, '..') +const sourceRoot = join(packageRoot, 'src') +const typesRoot = join(packageRoot, 'dist/types') + +for (const [source, target] of [ + ['Subscribe.tsrx.d.ts', 'Subscribe.d.ts'], + ['createTableHook.tsrx.d.ts', 'createTableHook.d.ts'], + ['useTable.tsrx.d.ts', 'useTable.d.ts'], +]) { + copyFileSync(join(sourceRoot, source), join(typesRoot, target)) +} + +const indexPath = join(typesRoot, 'index.d.ts') +writeFileSync( + indexPath, + readFileSync(indexPath, 'utf8').replaceAll( + /(Subscribe|createTableHook|useTable)\.tsrx/g, + '$1', + ), +) + +for (const file of readdirSync(typesRoot)) { + if (!file.endsWith('.d.ts')) continue + const source = readFileSync(join(typesRoot, file), 'utf8') + if (/(?:from\s+|import\s*\()['"][^'"]*\.tsrx['"]/.test(source)) { + throw new Error(`Declaration output retained a TSRX import: ${file}`) + } +} + +function walk(directory) { + return readdirSync(directory).flatMap((entry) => { + const path = join(directory, entry) + return statSync(path).isDirectory() ? walk(path) : [path] + }) +} + +const clientRoot = join(packageRoot, 'dist/client') +const serverRoot = join(packageRoot, 'dist/server') +const runtimeFiles = [...walk(clientRoot), ...walk(serverRoot)] + +for (const file of runtimeFiles) { + if (file.endsWith('.map')) { + throw new Error(`Build emitted a source map: ${file}`) + } + if (!file.endsWith('.js')) continue + + const source = readFileSync(file, 'utf8') + if (/(?:from\s+|import\s*\()['"][^'"]*\.tsrx['"]/.test(source)) { + throw new Error(`Runtime output retained a TSRX import: ${file}`) + } + if ( + file.startsWith(clientRoot) && + /from\s+['"]octane\/server['"]/.test(source) + ) { + throw new Error(`Client output imported the Octane server runtime: ${file}`) + } + if (file.startsWith(serverRoot) && /from\s+['"]octane['"]/.test(source)) { + throw new Error(`Server output imported the Octane client runtime: ${file}`) + } +} diff --git a/packages/octane-table/skills/create-table-hook/SKILL.md b/packages/octane-table/skills/create-table-hook/SKILL.md index e94eead5fa..0637ed8fd7 100644 --- a/packages/octane-table/skills/create-table-hook/SKILL.md +++ b/packages/octane-table/skills/create-table-hook/SKILL.md @@ -170,4 +170,4 @@ Source: `packages/octane-table/src/createTableHook.tsrx` ## API Discovery -Inspect `node_modules/@tanstack/octane-table/src/createTableHook.tsrx.d.ts`, `createTableHookContexts.ts`, and `types.ts` for exact return names, wrapper props, registries, and scoped context types. +Inspect `node_modules/@tanstack/octane-table/dist/createTableHook.d.ts`, `createTableHookContexts.d.ts`, and `types.d.ts` for exact return names, wrapper props, registries, and scoped context types. diff --git a/packages/octane-table/skills/getting-started/SKILL.md b/packages/octane-table/skills/getting-started/SKILL.md index a0f255469a..02a1be8b57 100644 --- a/packages/octane-table/skills/getting-started/SKILL.md +++ b/packages/octane-table/skills/getting-started/SKILL.md @@ -17,7 +17,7 @@ sources: - 'TanStack/table:packages/octane-table/src/useTable.tsrx' --- -This skill builds on `@tanstack/table-core#core` and `@tanstack/table-core#table-features`. The adapter publishes authored TSRX, so the consuming app must compile it with Octane's Vite integration. +This skill builds on `@tanstack/table-core#core` and `@tanstack/table-core#table-features`. The adapter ships compiled JavaScript; configure Octane's Vite integration to compile the application's TSRX components. ## Setup @@ -167,4 +167,4 @@ Source: `examples/octane/basic-use-table` ## API Discovery -Inspect `node_modules/@tanstack/octane-table/src/index.ts`, then the specific `*.tsrx.d.ts` sidecars and `types.ts`. This package intentionally ships authored source rather than a client-only `dist` build; follow core exports into installed `@tanstack/table-core/dist/`. +Inspect `node_modules/@tanstack/octane-table/dist/types/index.d.ts`, then `useTable.d.ts`, `Subscribe.d.ts`, `createTableHook.d.ts`, or `FlexRender.d.ts`; follow core exports into installed `@tanstack/table-core/dist/`. diff --git a/packages/octane-table/skills/table-state/SKILL.md b/packages/octane-table/skills/table-state/SKILL.md index a51acdf4b4..d316106d8d 100644 --- a/packages/octane-table/skills/table-state/SKILL.md +++ b/packages/octane-table/skills/table-state/SKILL.md @@ -162,4 +162,4 @@ Source: `docs/framework/octane/guide/table-state.md` ## API Discovery -Inspect `node_modules/@tanstack/octane-table/src/useTable.tsrx.d.ts`, `Subscribe.tsrx.d.ts`, and `types.ts`. Use `@tanstack/octane-store`, not another framework's Store hooks. +Inspect `node_modules/@tanstack/octane-table/dist/useTable.d.ts`, `Subscribe.d.ts`, and `types.d.ts`. Use `@tanstack/octane-store`, not another framework's Store hooks. diff --git a/packages/octane-table/src/Subscribe.tsrx b/packages/octane-table/src/Subscribe.tsrx index b8b7135db5..2cbd6c738d 100644 --- a/packages/octane-table/src/Subscribe.tsrx +++ b/packages/octane-table/src/Subscribe.tsrx @@ -59,5 +59,3 @@ function SubscribeImpl(props: SubscribeProps) @{ * ``` */ export const Subscribe = SubscribeImpl as SubscribeComponent; - - diff --git a/packages/octane-table/src/Subscribe.tsrx.d.ts b/packages/octane-table/src/Subscribe.tsrx.d.ts index 6913fec3ac..069ba5d1b7 100644 --- a/packages/octane-table/src/Subscribe.tsrx.d.ts +++ b/packages/octane-table/src/Subscribe.tsrx.d.ts @@ -1,8 +1,6 @@ -// Declaration companion for Subscribe.tsrx. -// -// A SPECIFIC module declaration (resolved by relative path), not an ambient -// `declare module '*.tsrx'` — so it types only this module and doesn't pollute a -// consumer's own .tsrx imports. The runtime resolves the real compiled .tsrx. +// Focused public declaration for the compiled Subscribe client/server modules. +// Keeping this specific to Subscribe avoids a wildcard ambient declaration for +// consumer-authored .tsrx modules. import type { SubscribeComponent } from './types' export declare const Subscribe: SubscribeComponent diff --git a/packages/octane-table/src/createTableHook.tsrx b/packages/octane-table/src/createTableHook.tsrx index b5131097ff..275c39a5ce 100644 --- a/packages/octane-table/src/createTableHook.tsrx +++ b/packages/octane-table/src/createTableHook.tsrx @@ -2,7 +2,7 @@ // hooks (`useRef`, `useMemo`). Every type lives in `./types`, so the // `.tsrx.d.ts` sidecar only restates this one signature. import { createColumnHelper as coreCreateColumnHelper } from '@tanstack/table-core'; -import { createContext, createElement, useContext, useMemo, useRef } from 'octane'; +import { createContext, useContext, useMemo, useRef } from 'octane'; import { FlexRender } from './FlexRender'; import { splitSlot } from './internal'; import { useTable } from './useTable.tsrx'; @@ -325,15 +325,13 @@ export function createTableHook< ) { const { children, selector: appTableSelector } = props as any; const currentTable = tableRef.current; + const TableSubscribe = currentTable.Subscribe as any; - // Built in setup with `createElement` so the template stays a single - // renderable hole rather than branching JSX inside the Provider. const body = appTableSelector - ? createElement(currentTable.Subscribe as any, { - selector: appTableSelector, - children: (state: TAppTableSelected) => children(state), - }) + ? + {(state: TAppTableSelected) => children(state)} + : children; return {body}; @@ -349,6 +347,7 @@ export function createTableHook< ) { const { cell, children, selector: appCellSelector } = props as any; const currentTable = tableRef.current; + const TableSubscribe = currentTable.Subscribe as any; const extendedCell = Object.assign(cell, { FlexRender: CellFlexRender, ...cellComponents, @@ -356,10 +355,9 @@ export function createTableHook< const body = appCellSelector - ? createElement(currentTable.Subscribe as any, { - selector: appCellSelector, - children: (state: TAppCellSelected) => children(extendedCell, state), - }) + ? + {(state: TAppCellSelected) => children(extendedCell, state)} + : children(extendedCell); return {body}; @@ -381,6 +379,7 @@ export function createTableHook< ) { const { header, children, selector: appHeaderSelector } = props as any; const currentTable = tableRef.current; + const TableSubscribe = currentTable.Subscribe as any; const extendedHeader = Object.assign(header, { FlexRender: HeaderFlexRender, ...headerComponents, @@ -388,10 +387,9 @@ export function createTableHook< const body = appHeaderSelector - ? createElement(currentTable.Subscribe as any, { - selector: appHeaderSelector, - children: (state: TAppHeaderSelected) => children(extendedHeader, state), - }) + ? + {(state: TAppHeaderSelected) => children(extendedHeader, state)} + : children(extendedHeader); return {body}; @@ -414,6 +412,7 @@ export function createTableHook< ) { const { header, children, selector: appFooterSelector } = props as any; const currentTable = tableRef.current; + const TableSubscribe = currentTable.Subscribe as any; const extendedHeader = Object.assign(header, { FlexRender: FooterFlexRender, ...headerComponents, @@ -421,10 +420,9 @@ export function createTableHook< const body = appFooterSelector - ? createElement(currentTable.Subscribe as any, { - selector: appFooterSelector, - children: (state: TAppFooterSelected) => children(extendedHeader, state), - }) + ? + {(state: TAppFooterSelected) => children(extendedHeader, state)} + : children(extendedHeader); return {body}; diff --git a/packages/octane-table/src/createTableHook.tsrx.d.ts b/packages/octane-table/src/createTableHook.tsrx.d.ts index 9eec97cdea..c7b98c50b7 100644 --- a/packages/octane-table/src/createTableHook.tsrx.d.ts +++ b/packages/octane-table/src/createTableHook.tsrx.d.ts @@ -1,8 +1,6 @@ -// Declaration companion for createTableHook.tsrx. -// -// A SPECIFIC module declaration (resolved by relative path), not an ambient -// `declare module '*.tsrx'` — so it types only this module and doesn't pollute a -// consumer's own .tsrx imports. The runtime resolves the real compiled .tsrx. +// Focused public declaration for the compiled createTableHook client/server +// modules. Keeping this specific to createTableHook avoids a wildcard ambient +// declaration for consumer-authored .tsrx modules. import type { TableFeatures } from '@tanstack/table-core' import type { CreateTableHookOptions, diff --git a/packages/octane-table/src/index.d.ts b/packages/octane-table/src/index.d.ts deleted file mode 100644 index 1dac9576c4..0000000000 --- a/packages/octane-table/src/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from '@tanstack/table-core' - -export * from './FlexRender' -export * from './Subscribe.tsrx' -export * from './createTableHook.tsrx' -export * from './createTableHookContexts' -export * from './types' -export * from './useTable.tsrx' diff --git a/packages/octane-table/src/index.ts b/packages/octane-table/src/index.ts index 02b6c07284..e3b2484a3c 100644 --- a/packages/octane-table/src/index.ts +++ b/packages/octane-table/src/index.ts @@ -6,9 +6,6 @@ // core verbatim and ports the adapter — `useTable`, `Subscribe`, `FlexRender`, // `createTableHook`, `createTableHookContexts` — onto octane. // -// Upstream's `useLegacyTable` v8-compat layer is deliberately NOT ported: it -// exists to migrate existing React v8 codebases, which an octane binding has -// none of. Octane code targets the v9 `useTable` API directly. export * from '@tanstack/table-core' export * from './FlexRender' diff --git a/packages/octane-table/src/useTable.tsrx.d.ts b/packages/octane-table/src/useTable.tsrx.d.ts index d9840c14f9..85326c4198 100644 --- a/packages/octane-table/src/useTable.tsrx.d.ts +++ b/packages/octane-table/src/useTable.tsrx.d.ts @@ -1,8 +1,6 @@ -// Declaration companion for useTable.tsrx. -// -// A SPECIFIC module declaration (resolved by relative path), not an ambient -// `declare module '*.tsrx'` — so it types only this module and doesn't pollute a -// consumer's own .tsrx imports. The runtime resolves the real compiled .tsrx. +// Focused public declaration for the compiled useTable client/server modules. +// Keeping this specific to useTable avoids a wildcard ambient declaration for +// consumer-authored .tsrx modules. import type { RowData, TableFeatures, diff --git a/packages/octane-table/tests/conformance/parity.test.ts b/packages/octane-table/tests/conformance/parity.test.ts index a3169446be..83c393811d 100644 --- a/packages/octane-table/tests/conformance/parity.test.ts +++ b/packages/octane-table/tests/conformance/parity.test.ts @@ -3,7 +3,7 @@ import * as core from '@tanstack/table-core' import * as binding from '@tanstack/octane-table' describe('export surface', () => { - it('re-exports Table core and the Octane adapter APIs without legacy APIs', () => { + it('re-exports Table core and the Octane adapter APIs', () => { const adapterExports = [ 'FlexRender', 'Subscribe', @@ -15,7 +15,6 @@ describe('export surface', () => { const expected = [...Object.keys(core), ...adapterExports].sort() expect(Object.keys(binding).sort()).toEqual(expected) - expect(binding).not.toHaveProperty('useLegacyTable') }) it('re-exports the same @tanstack/table-core module instance', () => { diff --git a/packages/octane-table/tests/type-tests/public-surface.tsx b/packages/octane-table/tests/type-tests/public-surface.tsx index cd148a6c1e..3ff8e224d5 100644 --- a/packages/octane-table/tests/type-tests/public-surface.tsx +++ b/packages/octane-table/tests/type-tests/public-surface.tsx @@ -17,9 +17,6 @@ import type { TableState, } from '@tanstack/octane-table' -// @ts-expect-error Octane intentionally has no legacy adapter subpath. -type LegacyAdapter = typeof import('@tanstack/octane-table/legacy') - type Row = { id: string; label: string } const features = tableFeatures({ rowSelectionFeature }) const columns: Array> = [ @@ -97,4 +94,3 @@ void checkedUseTable void checkedSubscribe void checkedFactory void invalidSubscribe -void (null as LegacyAdapter | null) diff --git a/packages/octane-table/tsconfig.build.json b/packages/octane-table/tsconfig.build.json new file mode 100644 index 0000000000..19db8b5174 --- /dev/null +++ b/packages/octane-table/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": false, + "emitDeclarationOnly": true, + "noEmit": false, + "outDir": "./dist/types", + "rootDir": "./src", + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/octane-table/tsconfig.json b/packages/octane-table/tsconfig.json index 961c4d8632..ce5e56e05b 100644 --- a/packages/octane-table/tsconfig.json +++ b/packages/octane-table/tsconfig.json @@ -12,6 +12,9 @@ "jsx": "react-jsx", "jsxImportSource": "octane", "allowImportingTsExtensions": true, + "paths": { + "@tanstack/octane-table": ["./src/index.ts"] + }, "types": ["node", "vitest/globals"] }, "include": ["src", "tests"] diff --git a/packages/octane-table/tsdown.config.ts b/packages/octane-table/tsdown.config.ts new file mode 100644 index 0000000000..74d34a87e5 --- /dev/null +++ b/packages/octane-table/tsdown.config.ts @@ -0,0 +1,71 @@ +import { createOctaneCompiler } from 'octane/compiler/bundler' +import { defineConfig } from 'tsdown' + +const runtimeEntries = [ + './src/index.ts', + './src/static-functions.ts', + './src/flex-render.ts', + './src/experimental-worker-plugin.ts', +] + +function octaneCompiler(environment: 'client' | 'server') { + const compiler = createOctaneCompiler({ + root: import.meta.dirname, + environment, + }) + + return { + name: `octane-${environment}`, + resolveId: { + order: 'pre' as const, + handler(source: string) { + if (environment === 'server' && source === 'octane') { + return { id: 'octane/server', external: true } + } + return null + }, + }, + transform(code: string, id: string) { + const result = compiler.transform(code, id, { + dev: false, + environment, + hmr: false, + }) + + return result && 'code' in result + ? { code: result.code, map: result.map } + : null + }, + } +} + +const runtimeConfig = { + entry: runtimeEntries, + format: ['esm'] as const, + unbundle: true, + dts: false, + sourcemap: false, + clean: true, + minify: false, + fixedExtension: false, + banner: { + js: '// octane-no-slot: this package already contains compiler-assigned hook slots.', + }, +} + +export default defineConfig([ + { + ...runtimeConfig, + name: 'client', + outDir: './dist/client', + platform: 'browser', + plugins: [octaneCompiler('client')], + }, + { + ...runtimeConfig, + name: 'server', + outDir: './dist/server', + platform: 'node', + plugins: [octaneCompiler('server')], + }, +]) diff --git a/scripts/generateDocs.js b/scripts/generateDocs.js index 6e26a321bd..01ced64caa 100644 --- a/scripts/generateDocs.js +++ b/scripts/generateDocs.js @@ -37,8 +37,6 @@ await generateReferenceDocs({ }, { name: 'octane-table', - // Octane ships authored .tsrx. TypeDoc reads the package's public - // declaration entry, which resolves the focused .tsrx.d.ts companions. entryPoints: [ resolve(__dirname, '../packages/octane-table/docs-entry.d.ts'), ],