From 6c456006a32b92bf937b9af80f8f3efc3ec55fff Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Wed, 25 Feb 2026 21:41:23 +0100 Subject: [PATCH 1/4] feat: add solid table devtools --- .../solid-table-devtools/eslint.config.js | 8 +++ packages/solid-table-devtools/package.json | 67 +++++++++++++++++++ .../src/SolidTableDevtools.tsx | 25 +++++++ packages/solid-table-devtools/src/index.ts | 17 +++++ packages/solid-table-devtools/src/plugin.tsx | 34 ++++++++++ .../solid-table-devtools/src/production.ts | 6 ++ .../solid-table-devtools/tsconfig.build.json | 13 ++++ packages/solid-table-devtools/tsconfig.json | 8 +++ packages/solid-table-devtools/vite.config.ts | 6 ++ 9 files changed, 184 insertions(+) create mode 100644 packages/solid-table-devtools/eslint.config.js create mode 100644 packages/solid-table-devtools/package.json create mode 100644 packages/solid-table-devtools/src/SolidTableDevtools.tsx create mode 100644 packages/solid-table-devtools/src/index.ts create mode 100644 packages/solid-table-devtools/src/plugin.tsx create mode 100644 packages/solid-table-devtools/src/production.ts create mode 100644 packages/solid-table-devtools/tsconfig.build.json create mode 100644 packages/solid-table-devtools/tsconfig.json create mode 100644 packages/solid-table-devtools/vite.config.ts diff --git a/packages/solid-table-devtools/eslint.config.js b/packages/solid-table-devtools/eslint.config.js new file mode 100644 index 0000000000..892f5314df --- /dev/null +++ b/packages/solid-table-devtools/eslint.config.js @@ -0,0 +1,8 @@ +// @ts-check + +import rootConfig from '../../eslint.config.js' + +/** @type {any} */ +const config = [...rootConfig] + +export default config diff --git a/packages/solid-table-devtools/package.json b/packages/solid-table-devtools/package.json new file mode 100644 index 0000000000..eea909e0ab --- /dev/null +++ b/packages/solid-table-devtools/package.json @@ -0,0 +1,67 @@ +{ + "name": "@tanstack/solid-table-devtools", + "version": "9.0.0-alpha.11", + "description": "Solid devtools for TanStack Table.", + "author": "Tanner Linsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TanStack/table.git", + "directory": "packages/react-table-devtools" + }, + "homepage": "https://tanstack.com/table", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "solid", + "tanstack", + "table", + "devtools" + ], + "scripts": { + "clean": "rimraf ./build && rimraf ./dist", + "test:eslint": "eslint ./src", + "test:lib": "vitest --passWithNoTests", + "test:lib:dev": "pnpm test:lib --watch", + "test:types": "tsc", + "test:build": "publint --strict", + "build": "tsc -p tsconfig.build.json" + }, + "type": "module", + "types": "dist/index.d.ts", + "module": "dist/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./production": { + "import": { + "types": "./dist/production.d.ts", + "default": "./dist/production.js" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "engines": { + "node": ">=16" + }, + "files": [ + "dist", + "src" + ], + "dependencies": { + "@tanstack/devtools-utils": "^0.3.0", + "@tanstack/table-core": "workspace:*", + "@tanstack/table-devtools": "workspace:*" + }, + "devDependencies": { + "solid-js": "^1.9.11", + "vite-plugin-solid": "^2.11.10" + } +} diff --git a/packages/solid-table-devtools/src/SolidTableDevtools.tsx b/packages/solid-table-devtools/src/SolidTableDevtools.tsx new file mode 100644 index 0000000000..2add8e7819 --- /dev/null +++ b/packages/solid-table-devtools/src/SolidTableDevtools.tsx @@ -0,0 +1,25 @@ +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/solid' +import { createSolidPanel } from '@tanstack/devtools-utils/solid' +import { + setTableDevtoolsTarget, + TableDevtoolsCore, +} from '@tanstack/table-devtools' + +import type { RowData, Table, TableFeatures } from '@tanstack/table-core' + +export interface TableDevtoolsSolidInit< + TFeatures extends TableFeatures = TableFeatures, + TData extends RowData = RowData, +> extends DevtoolsPanelProps { + table?: Table +} + +const [TableDevtoolsPanelBase, TableDevtoolsPanelNoOp] = + createSolidPanel(TableDevtoolsCore) + +function TableDevtoolsPanel(props: TableDevtoolsSolidInit) { + setTableDevtoolsTarget(props.table) + return +} + +export { TableDevtoolsPanel, TableDevtoolsPanelNoOp } diff --git a/packages/solid-table-devtools/src/index.ts b/packages/solid-table-devtools/src/index.ts new file mode 100644 index 0000000000..c34bc8c874 --- /dev/null +++ b/packages/solid-table-devtools/src/index.ts @@ -0,0 +1,17 @@ +'use client' + +import * as Devtools from './SolidTableDevtools' +import * as plugin from './plugin' + +export const TableDevtoolsPanel = + process.env.NODE_ENV !== 'development' + ? Devtools.TableDevtoolsPanelNoOp + : Devtools.TableDevtoolsPanel + +export const tableDevtoolsPlugin = + process.env.NODE_ENV !== 'development' + ? plugin.tableDevtoolsNoOpPlugin + : plugin.tableDevtoolsPlugin + +export type { TableDevtoolsSolidInit } from './SolidTableDevtools' +export type { TableDevtoolsPluginOptions } from './plugin' diff --git a/packages/solid-table-devtools/src/plugin.tsx b/packages/solid-table-devtools/src/plugin.tsx new file mode 100644 index 0000000000..dfc00d2760 --- /dev/null +++ b/packages/solid-table-devtools/src/plugin.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import { TableDevtoolsPanel } from './SolidTableDevtools' +import type { RowData, Table, TableFeatures } from '@tanstack/table-core' + +export interface TableDevtoolsPluginOptions< + TFeatures extends TableFeatures = TableFeatures, + TData extends RowData = RowData, +> { + table: Table +} + +function tableDevtoolsPlugin< + TFeatures extends TableFeatures = TableFeatures, + TData extends RowData = RowData, +>(options: TableDevtoolsPluginOptions) { + return { + name: 'TanStack Table', + render: (_el: HTMLElement, theme: 'light' | 'dark') => ( + + ), + } +} + +function tableDevtoolsNoOpPlugin< + TFeatures extends TableFeatures = TableFeatures, + TData extends RowData = RowData, +>(_options: TableDevtoolsPluginOptions) { + return { + name: 'TanStack Table', + render: (_el: HTMLElement, _theme: 'light' | 'dark') => <>, + } +} + +export { tableDevtoolsPlugin, tableDevtoolsNoOpPlugin } diff --git a/packages/solid-table-devtools/src/production.ts b/packages/solid-table-devtools/src/production.ts new file mode 100644 index 0000000000..b197295f30 --- /dev/null +++ b/packages/solid-table-devtools/src/production.ts @@ -0,0 +1,6 @@ +'use client' + +export { TableDevtoolsPanel } from './SolidTableDevtools' +export type { TableDevtoolsSolidInit } from './SolidTableDevtools' +export { tableDevtoolsPlugin } from './plugin' +export type { TableDevtoolsPluginOptions } from './plugin' diff --git a/packages/solid-table-devtools/tsconfig.build.json b/packages/solid-table-devtools/tsconfig.build.json new file mode 100644 index 0000000000..93755fa253 --- /dev/null +++ b/packages/solid-table-devtools/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "rootDir": "src", + "outDir": "dist", + "noEmit": false, + "declaration": true, + "sourceMap": true + }, + "include": ["src"] +} diff --git a/packages/solid-table-devtools/tsconfig.json b/packages/solid-table-devtools/tsconfig.json new file mode 100644 index 0000000000..966bd728f5 --- /dev/null +++ b/packages/solid-table-devtools/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js" + }, + "include": ["src", "eslint.config.js", "vite.config.ts", "tests"] +} diff --git a/packages/solid-table-devtools/vite.config.ts b/packages/solid-table-devtools/vite.config.ts new file mode 100644 index 0000000000..a872ffe359 --- /dev/null +++ b/packages/solid-table-devtools/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vitest/config' +import solid from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [solid()], +}) From 2e03614daca45ef34cd294a4192d47c2ef904be9 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Wed, 25 Feb 2026 21:41:48 +0100 Subject: [PATCH 2/4] feat: add table devtools options panel --- packages/table-devtools/package.json | 2 + .../src/TableContextProvider.tsx | 7 +-- .../src/components/OptionsPanel.tsx | 40 ++++++++++++++ .../table-devtools/src/components/Shell.tsx | 5 ++ pnpm-lock.yaml | 55 +++++++++++++++++-- 5 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 packages/table-devtools/src/components/OptionsPanel.tsx diff --git a/packages/table-devtools/package.json b/packages/table-devtools/package.json index 61034c8897..764f5c2e59 100644 --- a/packages/table-devtools/package.json +++ b/packages/table-devtools/package.json @@ -58,6 +58,8 @@ "dependencies": { "@tanstack/devtools-ui": "^0.4.4", "@tanstack/devtools-utils": "^0.3.0", + "@tanstack/solid-store": "^0.9.1", + "clsx": "^2.1.1", "goober": "^2.1.18", "solid-js": "^1.9.11" }, diff --git a/packages/table-devtools/src/TableContextProvider.tsx b/packages/table-devtools/src/TableContextProvider.tsx index 540c5e3484..7a7c1d0f8e 100644 --- a/packages/table-devtools/src/TableContextProvider.tsx +++ b/packages/table-devtools/src/TableContextProvider.tsx @@ -9,12 +9,11 @@ import { getTableDevtoolsTarget, subscribeTableDevtoolsTarget, } from './tableTarget' - import type { Accessor, ParentComponent, Setter } from 'solid-js' -import type { RowData, Table, TableFeatures } from '@tanstack/table-core' +import type { RowData, Table, stockFeatures } from '@tanstack/table-core' -type TableDevtoolsTabId = 'features' | 'state' | 'rows' | 'columns' -type AnyTable = Table +export type TableDevtoolsTabId = 'features' | 'state' | 'options' | 'rows' | 'columns' +type AnyTable = Table interface TableDevtoolsContextValue { table: Accessor diff --git a/packages/table-devtools/src/components/OptionsPanel.tsx b/packages/table-devtools/src/components/OptionsPanel.tsx new file mode 100644 index 0000000000..011be836e8 --- /dev/null +++ b/packages/table-devtools/src/components/OptionsPanel.tsx @@ -0,0 +1,40 @@ +import { JsonTree } from '@tanstack/devtools-ui' +import { useStore } from '@tanstack/solid-store' +import { useTableDevtoolsContext } from '../TableContextProvider' +import { useStyles } from '../styles/use-styles' +import { ResizableSplit } from './ResizableSplit' + +export function OptionsPanel() { + const styles = useStyles() + const { table } = useTableDevtoolsContext() + + const tableInstance = table() + const tableState = tableInstance + ? useStore( + tableInstance.baseOptionsStore, + ({ state, data, _features, _rowModels, ...options }) => options, + ) + : undefined + + const getState = (): unknown => { + tableState?.() + if (!tableInstance) { + return undefined + } + return tableState?.() + } + + return ( +
+ +
Options
+ + + } + right={<>} + /> +
+ ) +} diff --git a/packages/table-devtools/src/components/Shell.tsx b/packages/table-devtools/src/components/Shell.tsx index cdbb53bdfd..aab783da08 100644 --- a/packages/table-devtools/src/components/Shell.tsx +++ b/packages/table-devtools/src/components/Shell.tsx @@ -6,10 +6,12 @@ import { ColumnsPanel } from './ColumnsPanel' import { FeaturesPanel } from './FeaturesPanel' import { RowsPanel } from './RowsPanel' import { StatePanel } from './StatePanel' +import { OptionsPanel } from './OptionsPanel' const tabs = [ { id: 'features', label: 'Features' }, { id: 'state', label: 'State' }, + { id: 'options', label: 'Options' }, { id: 'rows', label: 'Rows' }, { id: 'columns', label: 'Columns' }, ] as const @@ -54,6 +56,9 @@ export function Shell() { + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d8980058c..371e41ebaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2778,9 +2778,15 @@ importers: examples/solid/row-selection: dependencies: + '@tanstack/solid-devtools': + specifier: ^0.7.26 + version: 0.7.26(csstype@3.2.3)(solid-js@1.9.11) '@tanstack/solid-table': specifier: ^9.0.0-alpha.10 version: link:../../../packages/solid-table + '@tanstack/solid-table-devtools': + specifier: 9.0.0-alpha.11 + version: link:../../../packages/solid-table-devtools solid-js: specifier: ^1.9.11 version: 1.9.11 @@ -3591,11 +3597,30 @@ importers: packages/solid-table: dependencies: '@tanstack/solid-store': - specifier: ^0.8.0 - version: 0.8.1(solid-js@1.9.11) + specifier: ^0.9.1 + version: 0.9.1(solid-js@1.9.11) + '@tanstack/table-core': + specifier: workspace:* + version: link:../table-core + devDependencies: + solid-js: + specifier: ^1.9.11 + version: 1.9.11 + vite-plugin-solid: + specifier: ^2.11.10 + version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2)) + + packages/solid-table-devtools: + dependencies: + '@tanstack/devtools-utils': + specifier: ^0.3.0 + version: 0.3.0(@types/react@19.2.10)(csstype@3.2.3)(preact@10.28.2)(react@19.2.4)(solid-js@1.9.11)(vue@3.5.27(typescript@5.9.3)) '@tanstack/table-core': specifier: workspace:* version: link:../table-core + '@tanstack/table-devtools': + specifier: workspace:* + version: link:../table-devtools devDependencies: solid-js: specifier: ^1.9.11 @@ -3643,6 +3668,9 @@ importers: '@tanstack/devtools-utils': specifier: ^0.3.0 version: 0.3.0(@types/react@19.2.10)(csstype@3.2.3)(preact@10.28.2)(react@19.2.4)(solid-js@1.9.11)(vue@3.5.27(typescript@5.9.3)) + '@tanstack/solid-store': + specifier: ^0.9.1 + version: 0.9.1(solid-js@1.9.11) '@tanstack/table-core': specifier: workspace:* version: link:../table-core @@ -7216,8 +7244,14 @@ packages: resolution: {integrity: sha512-Lvq5VnH9Rtqci0urHENMMlyswN5fySvIANclS7cUq2xOr5Cc9QfPpwctZ3Bi1X+MibKIcXplTAixIYDFyYquQg==} engines: {node: '>=12'} - '@tanstack/solid-store@0.8.1': - resolution: {integrity: sha512-1p4TTJGIZJ2J7130aTo7oWfHVRSCd9DxLP3HzcDMnzn56pz8krlyBEzsE+z/sHGXP0EC/JjT02fgj2L9+fmf8Q==} + '@tanstack/solid-devtools@0.7.26': + resolution: {integrity: sha512-HagP7imKP6I+GVaHZp3GvwKUZ4qTHkMmZVOK6+uFrlDDJNtKBEblMW1jyGsh0NKrbajTW2KNT0AlylRu5vsR/A==} + engines: {node: '>=18'} + peerDependencies: + solid-js: '>=1.9.7' + + '@tanstack/solid-store@0.9.1': + resolution: {integrity: sha512-gx7ToM+Yrkui36NIj0HjAufzv1Dg8usjtVFy5H3Ll52Xjuz+eliIJL+ihAr4LRuWh3nDPBR+nCLW0ShFrbE5yw==} peerDependencies: solid-js: ^1.6.0 @@ -15720,9 +15754,18 @@ snapshots: - vite-plugin-solid - webpack - '@tanstack/solid-store@0.8.1(solid-js@1.9.11)': + '@tanstack/solid-devtools@0.7.26(csstype@3.2.3)(solid-js@1.9.11)': dependencies: - '@tanstack/store': 0.8.1 + '@tanstack/devtools': 0.10.7(csstype@3.2.3)(solid-js@1.9.11) + solid-js: 1.9.11 + transitivePeerDependencies: + - bufferutil + - csstype + - utf-8-validate + + '@tanstack/solid-store@0.9.1(solid-js@1.9.11)': + dependencies: + '@tanstack/store': 0.9.1 solid-js: 1.9.11 '@tanstack/store@0.7.7': {} From 84822c15cc7f8f00973cd9f45c6565a7bf2acced Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Wed, 25 Feb 2026 22:26:06 +0100 Subject: [PATCH 3/4] feat: move reactivity feature to core --- examples/solid/row-selection/package.json | 2 + examples/solid/row-selection/src/App.tsx | 51 +++-- packages/angular-table/src/index.ts | 1 - packages/angular-table/src/injectTable.ts | 22 +- .../angular-table/tests/injectTable.test.ts | 2 +- .../tests/reactivityUtils.test.ts | 210 ------------------ packages/solid-table/package.json | 2 +- packages/solid-table/src/createTable.ts | 106 +++++++-- packages/solid-table/src/createTableHelper.ts | 9 +- .../src/core/table/constructTable.ts | 12 +- .../src/core/table/coreTablesFeature.types.ts | 6 +- .../src/core/table/coreTablesFeature.utils.ts | 6 +- .../tableReactivityFeature.ts} | 111 +++++---- .../tableReactivityFeature.utils.ts} | 73 +++--- packages/table-core/src/index.ts | 4 + .../src/TableContextProvider.tsx | 11 +- pnpm-lock.yaml | 3 + 17 files changed, 283 insertions(+), 348 deletions(-) delete mode 100644 packages/angular-table/tests/reactivityUtils.test.ts rename packages/{angular-table/src/angularReactivityFeature.ts => table-core/src/features/table-reactivity/tableReactivityFeature.ts} (67%) rename packages/{angular-table/src/reactivityUtils.ts => table-core/src/features/table-reactivity/tableReactivityFeature.utils.ts} (83%) diff --git a/examples/solid/row-selection/package.json b/examples/solid/row-selection/package.json index 82cbf8ce2b..8fdf7ab281 100644 --- a/examples/solid/row-selection/package.json +++ b/examples/solid/row-selection/package.json @@ -18,6 +18,8 @@ }, "dependencies": { "@tanstack/solid-table": "^9.0.0-alpha.10", + "@tanstack/solid-devtools": "^0.7.26", + "@tanstack/solid-table-devtools": "9.0.0-alpha.11", "solid-js": "^1.9.11" } } diff --git a/examples/solid/row-selection/src/App.tsx b/examples/solid/row-selection/src/App.tsx index 57991b86f0..7e22049531 100644 --- a/examples/solid/row-selection/src/App.tsx +++ b/examples/solid/row-selection/src/App.tsx @@ -1,3 +1,9 @@ +import type { + Column, + ColumnDef, + SolidTable, + Table, +} from '@tanstack/solid-table' import { columnFilteringFeature, createFilteredRowModel, @@ -11,15 +17,11 @@ import { tableFeatures, } from '@tanstack/solid-table' import { For, Show, createEffect, createSignal } from 'solid-js' -import { makeData } from './makeData' -import type { - Column, - ColumnDef, - SolidTable, - Table, -} from '@tanstack/solid-table' import type { Person } from './makeData' +import { makeData } from './makeData' import './index.css' +import { TanStackDevtools } from '@tanstack/solid-devtools' +import { tableDevtoolsPlugin } from '@tanstack/solid-table-devtools' export const _features = tableFeatures({ rowPaginationFeature, @@ -51,16 +53,18 @@ function App() { ) }, - cell: ({ row }) => ( -
- -
- ), + cell: ({ row }) => { + return ( +
+ +
+ ) + }, }, { header: 'Name', @@ -113,6 +117,8 @@ function App() { }, ] + const [enableRowSelection, setEnableRowSelection] = createSignal(true) + table = createTable({ _features, _rowModels: { @@ -124,11 +130,14 @@ function App() { }, columns, 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 + get enableRowSelection() { + return enableRowSelection() + }, debugTable: true, }) + window.setEnable = setEnableRowSelection + return ( // ({ @@ -140,6 +149,8 @@ function App() { // > // {(state) => (
+ +
({ globalFilter: state.globalFilter })} @@ -333,8 +344,6 @@ function App() {
- // )} - //
) } diff --git a/packages/angular-table/src/index.ts b/packages/angular-table/src/index.ts index e51f5d5043..6147ee1b65 100644 --- a/packages/angular-table/src/index.ts +++ b/packages/angular-table/src/index.ts @@ -3,7 +3,6 @@ import { FlexRenderDirective } from './flexRender' export * from '@tanstack/table-core' -export * from './angularReactivityFeature' export * from './flexRender' export * from './injectTable' export * from './flex-render/flexRenderComponent' diff --git a/packages/angular-table/src/injectTable.ts b/packages/angular-table/src/injectTable.ts index c1cbc5d8fd..fec962fad1 100644 --- a/packages/angular-table/src/injectTable.ts +++ b/packages/angular-table/src/injectTable.ts @@ -3,12 +3,16 @@ import { assertInInjectionContext, computed, inject, + isSignal, + signal, untracked, } from '@angular/core' -import { constructTable } from '@tanstack/table-core' +import { + constructReactivityFeature, + constructTable, +} from '@tanstack/table-core' import { injectStore } from '@tanstack/angular-store' import { lazyInit } from './lazySignalInitializer' -import { angularReactivityFeature } from './angularReactivityFeature' import type { RowData, Table, @@ -104,6 +108,16 @@ export function injectTable< assertInInjectionContext(injectTable) const injector = inject(Injector) + const angularReactivityFeature = constructReactivityFeature({ + createSignal: (value) => { + return signal(value) as any + }, + createMemo: (fn) => { + return computed(() => fn()) + }, + isSignal: (value) => isSignal(value), + }) + return lazyInit(() => { const resolvedOptions: TableOptions = { ...options(), @@ -142,13 +156,15 @@ export function injectTable< const tableSignalNotifier = computed( () => { tableState() - table.setOptions(updatedOptions()) + const newOptions = updatedOptions() + untracked(() => table.setOptions(newOptions)) untracked(() => table.baseStore.setState((prev) => ({ ...prev }))) return table }, { equal: () => false }, ) + // @ts-ignore table.setTableNotifier(tableSignalNotifier) table.Subscribe = function Subscribe(props: { diff --git a/packages/angular-table/tests/injectTable.test.ts b/packages/angular-table/tests/injectTable.test.ts index 1f74844c66..e2700dcb8a 100644 --- a/packages/angular-table/tests/injectTable.test.ts +++ b/packages/angular-table/tests/injectTable.test.ts @@ -75,7 +75,7 @@ describe('injectTable', () => { }) test('supports "Object.keys"', () => { - const keys = Object.keys(table.get()).concat('state') + const keys = Object.keys(table.value()).concat('state') expect(Object.keys(table)).toEqual(keys) }) diff --git a/packages/angular-table/tests/reactivityUtils.test.ts b/packages/angular-table/tests/reactivityUtils.test.ts deleted file mode 100644 index 5aff11bf23..0000000000 --- a/packages/angular-table/tests/reactivityUtils.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { describe, expect, test, vi } from 'vitest' -import { effect, isSignal, signal } from '@angular/core' -import { TestBed } from '@angular/core/testing' -import { defineLazyComputedProperty, toComputed } from '../src/reactivityUtils' - -describe('toComputed', () => { - describe('args = 0', () => { - test('creates a computed', () => { - const notifier = signal(1) - - const result = toComputed( - notifier, - () => { - return notifier() * 2 - }, - 'double', - ) - - expect(result.name).toEqual('double') - expect(isSignal(result)).toEqual(true) - - TestBed.runInInjectionContext(() => { - const mockFn = vi.fn() - - effect(() => { - mockFn(result()) - }) - - TestBed.tick() - expect(mockFn).toHaveBeenLastCalledWith(2) - - notifier.set(3) - TestBed.tick() - expect(mockFn).toHaveBeenLastCalledWith(6) - - notifier.set(2) - TestBed.tick() - expect(mockFn).toHaveBeenLastCalledWith(4) - - expect(mockFn.mock.calls.length).toEqual(3) - }) - }) - }) - - describe('args >= 1', () => { - test('creates a fn an explicit first argument and allows other args', () => { - const notifier = signal(1) - - const fn1 = toComputed( - notifier, - (arg0: number, arg1: string, arg3?: number) => { - return { arg0, arg1, arg3 } - }, - '3args', - ) - expect(fn1.length).toEqual(0) - - // currently full rest parameters is not supported - const fn2 = toComputed( - notifier, - function myFn(...args: Array) { - return args - }, - '3args', - ) - expect(fn2.length).toEqual(0) - }) - - test('reuse created computed when args are the same', () => { - const notifier = signal(1) - - const invokeMock = vi.fn() - - const sum = toComputed( - notifier, - (arg0: number, arg1?: string) => { - invokeMock(arg0) - return notifier() + arg0 - }, - 'sum', - ) - - sum(1) - sum(3) - sum(2) - sum(1) - sum(1) - sum(2) - sum(3) - - expect(invokeMock).toHaveBeenCalledTimes(3) - expect(invokeMock).toHaveBeenNthCalledWith(1, 1) - expect(invokeMock).toHaveBeenNthCalledWith(2, 3) - expect(invokeMock).toHaveBeenNthCalledWith(3, 2) - }) - - test('cached computed are reactive', () => { - const invokeMock = vi.fn() - const notifier = signal(1) - - const sum = toComputed( - notifier, - (arg0: number) => { - invokeMock(arg0) - return notifier() + arg0 - }, - 'sum', - ) - - TestBed.runInInjectionContext(() => { - const mockSumBy3Fn = vi.fn() - const mockSumBy2Fn = vi.fn() - - effect(() => { - mockSumBy3Fn(sum(3)) - }) - effect(() => { - mockSumBy2Fn(sum(2)) - }) - - TestBed.flushEffects() - expect(mockSumBy3Fn).toHaveBeenLastCalledWith(4) - expect(mockSumBy2Fn).toHaveBeenLastCalledWith(3) - - notifier.set(2) - TestBed.flushEffects() - expect(mockSumBy3Fn).toHaveBeenLastCalledWith(5) - expect(mockSumBy2Fn).toHaveBeenLastCalledWith(4) - - expect(mockSumBy3Fn.mock.calls.length).toEqual(2) - expect(mockSumBy2Fn.mock.calls.length).toEqual(2) - }) - - for (let i = 0; i < 4; i++) { - sum(3) - sum(2) - } - // invoked every time notifier change - expect(invokeMock).toHaveBeenCalledTimes(4) - }) - }) - - describe('args 0~1', () => { - test('creates a fn an explicit first argument and allows other args', () => { - const notifier = signal(1) - const captor = vi.fn<(arg0?: number) => void>() - const captor2 = vi.fn<(arg0?: number) => void>() - - const fn1 = toComputed( - notifier, - (arg0: number | undefined) => { - if (arg0 === undefined) { - return 5 * notifier() - } - return arg0 * notifier() - }, - 'optionalArgs', - ) - - expect(isSignal(fn1)).toEqual(false) - - TestBed.runInInjectionContext(() => { - effect(() => { - captor(fn1(0)) - }) - effect(() => { - captor2(fn1(1)) - }) - }) - - TestBed.tick() - notifier.set(2) - TestBed.tick() - notifier.set(3) - expect(captor.mock.calls).toHaveLength(1) - expect(captor2.mock.calls).toHaveLength(2) - expect(captor2).toHaveBeenNthCalledWith(1, 1) - expect(captor2).toHaveBeenNthCalledWith(2, 2) - }) - }) -}) - -describe('defineLazyComputedProperty', () => { - test('define a computed property and cache the result after first access', () => { - const notifier = signal(1) - const originalObject = {} as any - const mockValueFn = vi.fn(() => 2) - - defineLazyComputedProperty(notifier, { - originalObject, - property: 'computedProp', - valueFn: mockValueFn, - }) - - let propDescriptor = Object.getOwnPropertyDescriptor( - originalObject, - 'computedProp', - ) - expect(propDescriptor && !!propDescriptor.get).toEqual(true) - - originalObject.computedProp - - propDescriptor = Object.getOwnPropertyDescriptor( - originalObject, - 'computedProp', - ) - expect(propDescriptor!.get).not.toBeDefined() - expect(isSignal(propDescriptor!.value)) - }) -}) diff --git a/packages/solid-table/package.json b/packages/solid-table/package.json index 9638c02201..8162065bfa 100644 --- a/packages/solid-table/package.json +++ b/packages/solid-table/package.json @@ -52,7 +52,7 @@ "build": "tsc -p tsconfig.build.json" }, "dependencies": { - "@tanstack/solid-store": "^0.8.0", + "@tanstack/solid-store": "^0.9.1", "@tanstack/table-core": "workspace:*" }, "devDependencies": { diff --git a/packages/solid-table/src/createTable.ts b/packages/solid-table/src/createTable.ts index 0e91d32dab..13f68366be 100644 --- a/packages/solid-table/src/createTable.ts +++ b/packages/solid-table/src/createTable.ts @@ -1,6 +1,17 @@ -import { constructTable } from '@tanstack/table-core' +import { + constructReactivityFeature, + constructTable, +} from '@tanstack/table-core' import { useStore } from '@tanstack/solid-store' -import { createComputed, createSignal } from 'solid-js' +import { + createComputed, + createMemo, + createSignal, + getOwner, + mergeProps, + runWithOwner, +} from 'solid-js' +import type { Accessor, JSX } from 'solid-js' import type { NoInfer, RowData, @@ -9,7 +20,6 @@ import type { TableOptions, TableState, } from '@tanstack/table-core' -import type { Accessor, JSX } from 'solid-js' export type SolidTable< TFeatures extends TableFeatures, @@ -54,21 +64,75 @@ export function createTable< selector: (state: TableState) => TSelected = () => ({}) as TSelected, ): SolidTable { - const table = constructTable(tableOptions) as SolidTable< + const owner = getOwner() + + const solidReactivityFeature = constructReactivityFeature({ + createSignal: (value) => { + const signal = createSignal(value) + function interoperableSignal() { + return signal[0]() + } + return Object.assign(interoperableSignal, { + set: (value: any) => signal[1](() => value), + }) + }, + createMemo: (fn) => { + if (owner) { + return runWithOwner(owner, () => createMemo(fn))! + } + return createMemo(fn) + }, + isSignal: (value) => typeof value === 'function', + }) + + const mergedOptions = mergeProps(tableOptions, { + _features: mergeProps(tableOptions._features, { + solidReactivityFeature, + }), + }) as any + + const [renderVersion, setRenderVersion] = createSignal(0) + + const resolvedOptions = mergeProps( + { + mergeOptions: ( + defaultOptions: TableOptions, + options: TableOptions, + ) => { + return mergeProps(defaultOptions, options) + }, + }, + mergedOptions, + ) as TableOptions + + const table = constructTable(resolvedOptions) as SolidTable< TFeatures, TData, TSelected > + // @ts-ignore + table.setTableNotifier(() => { + renderVersion() + return table + }) + /** * Temp force reactivity to all state changes on every table.get* method */ const allState = useStore(table.store, (state) => state) - const [renderVersion, setRenderVersion] = createSignal(0) + const allOptions = useStore(table.baseOptionsStore, (options) => options) + + createComputed(() => { + table.setOptions((prev) => { + return mergeProps(prev, mergedOptions) as TableOptions + }) + }) createComputed(() => { // Access storeState to create reactive dependency allState() + allOptions() // Increment version to invalidate cached get* methods setRenderVersion((v) => v + 1) // Update options when store changes @@ -77,17 +141,27 @@ export function createTable< // }) }) - // Wrap all "get*" methods to make them reactive - Object.keys(table).forEach((key) => { - const value = (table as any)[key] - if (typeof value === 'function' && key.startsWith('get')) { - const originalMethod = value.bind(table) - ;(table as any)[key] = (...args: Array) => { - renderVersion() - return originalMethod(...args) - } - } - }) + // Object.assign(table, { + // get options() { + // allOptions() + // return table.baseOptionsStore.get() + // }, + // }) + // + // Object.defineProperty(table.store, 'get', { + // value: () => { + // allState() + // allOptions() + // return table.store['atom'].get() + // }, + // }) + // Object.defineProperty(table.store, 'state', { + // get() { + // allState() + // allOptions() + // return this['atom'].get() + // }, + // }) table.Subscribe = function Subscribe(props: { selector: (state: TableState) => TSelected diff --git a/packages/solid-table/src/createTableHelper.ts b/packages/solid-table/src/createTableHelper.ts index dfbe95ea1d..768899cf12 100644 --- a/packages/solid-table/src/createTableHelper.ts +++ b/packages/solid-table/src/createTableHelper.ts @@ -13,7 +13,7 @@ import type { export type TableHelper< TFeatures extends TableFeatures, TData extends RowData = any, -> = Omit, 'tableCreator'> & { +> = Omit, 'tableCreator'> & { createTable: ( tableOptions: Omit< TableOptions, @@ -27,9 +27,12 @@ export function createTableHelper< TFeatures extends TableFeatures, TData extends RowData = any, >( - tableHelperOptions: TableHelperOptions, + tableHelperOptions: TableHelperOptions, ): TableHelper { - const tableHelper = constructTableHelper(createTable, tableHelperOptions) + const tableHelper = constructTableHelper( + createTable as any, + tableHelperOptions, + ) return { ...tableHelper, createTable: ( diff --git a/packages/table-core/src/core/table/constructTable.ts b/packages/table-core/src/core/table/constructTable.ts index 927593da06..0b94cc6f85 100644 --- a/packages/table-core/src/core/table/constructTable.ts +++ b/packages/table-core/src/core/table/constructTable.ts @@ -42,10 +42,18 @@ export function constructTable< return Object.assign(obj, feature.getDefaultTableOptions?.(table)) }, {}) as TableOptions - table.options = { + table.baseOptionsStore = createStore({ ...defaultOptions, ...tableOptions, - } + }) + + Object.defineProperty(table, 'options', { + enumerable: true, + configurable: true, + get() { + return table.baseOptionsStore.state + }, + }) table.initialState = getInitialTableState( table._features, diff --git a/packages/table-core/src/core/table/coreTablesFeature.types.ts b/packages/table-core/src/core/table/coreTablesFeature.types.ts index 0124eac018..ccb1224ec1 100644 --- a/packages/table-core/src/core/table/coreTablesFeature.types.ts +++ b/packages/table-core/src/core/table/coreTablesFeature.types.ts @@ -94,6 +94,10 @@ export interface Table_CoreProperties< * The base store for the table. This can be used to write to the table state. */ baseStore: Store> + /** + * The base store for the table. This can be used to write to the table state. + */ + baseOptionsStore: Store> /** * This is the resolved initial state of the table. */ @@ -101,7 +105,7 @@ export interface Table_CoreProperties< /** * A read-only reference to the table's current options. */ - options: TableOptions + readonly options: TableOptions /** * Where the table state is stored. */ diff --git a/packages/table-core/src/core/table/coreTablesFeature.utils.ts b/packages/table-core/src/core/table/coreTablesFeature.utils.ts index 85c2cd7869..436a5f1e88 100644 --- a/packages/table-core/src/core/table/coreTablesFeature.utils.ts +++ b/packages/table-core/src/core/table/coreTablesFeature.utils.ts @@ -35,6 +35,8 @@ export function table_setOptions< table: Table_Internal, updater: Updater>, ): void { - const newOptions = functionalUpdate(updater, table.options) - table.options = table_mergeOptions(table, newOptions) + table.baseOptionsStore.setState((options) => { + const newOptions = functionalUpdate(updater, options) + return table_mergeOptions(table, newOptions) + }) } diff --git a/packages/angular-table/src/angularReactivityFeature.ts b/packages/table-core/src/features/table-reactivity/tableReactivityFeature.ts similarity index 67% rename from packages/angular-table/src/angularReactivityFeature.ts rename to packages/table-core/src/features/table-reactivity/tableReactivityFeature.ts index 76b3433310..230affbf23 100644 --- a/packages/angular-table/src/angularReactivityFeature.ts +++ b/packages/table-core/src/features/table-reactivity/tableReactivityFeature.ts @@ -1,24 +1,8 @@ -import { computed, signal } from '@angular/core' -import { setReactivePropertiesOnObject } from './reactivityUtils' -import type { Signal } from '@angular/core' -import type { - RowData, - Table, - TableFeature, - TableFeatures, -} from '@tanstack/table-core' - -declare module '@tanstack/table-core' { - interface TableOptions_Plugins< - TFeatures extends TableFeatures, - TData extends RowData, - > extends TableOptions_AngularReactivity {} - - interface Table_Plugins< - TFeatures extends TableFeatures, - TData extends RowData, - > extends Table_AngularReactivity {} -} +import { setReactivePropertiesOnObject } from './tableReactivityFeature.utils' +import type { TableFeature, TableFeatures } from '../../types/TableFeatures' +import type { Accessor } from './tableReactivityFeature.utils' +import type { RowData } from '../../types/type-utils' +import type { Table } from '../../types/Table' /** * Predicate used to skip/ignore a property name when applying Angular reactivity. @@ -68,7 +52,7 @@ export interface AngularReactivityFlags { * * Available on `createTable` options via module augmentation in this file. */ -interface TableOptions_AngularReactivity { +interface TableOptions_Reactivity { /** * Enables/disables and configures Angular reactivity on table-related prototypes. * @@ -82,31 +66,31 @@ interface TableOptions_AngularReactivity { * * Added to the table instance via module augmentation. */ -interface Table_AngularReactivity< +interface Table_Reactivity< TFeatures extends TableFeatures, TData extends RowData, > { /** * Returns a table signal that updates whenever the table state or options changes. */ - get: Signal> + value: Accessor> /** * Sets the reactive notifier that powers {@link get}. * * @internal Used by the Angular table adapter to connect its notifier to the core table. */ - setTableNotifier: (signal: Signal>) => void + setTableNotifier: (signal: Accessor>) => void } /** * Type map describing what this feature adds to TanStack Table constructors. */ -interface AngularReactivityFeatureConstructors< +interface TableReactivityFeatureConstructors< TFeatures extends TableFeatures, TData extends RowData, > { - TableOptions: TableOptions_AngularReactivity - Table: Table_AngularReactivity + TableOptions: TableOptions_Reactivity + Table: Table_Reactivity } /** @@ -127,10 +111,25 @@ const getUserSkipPropertyFn = ( return value ?? defaultPropertyFn } -function constructAngularReactivityFeature< +export type InteroperableWritableSignal = { + (): T + set: (value: unknown) => void +} + +export interface ReactivityFeatureFactoryOptions { + createSignal: (value: T) => InteroperableWritableSignal + createMemo: (accessor: Accessor) => Accessor + isSignal: (v: unknown) => boolean +} + +export function constructReactivityFeature< TFeatures extends TableFeatures, TData extends RowData, ->(): TableFeature> { +>( + factory: ReactivityFeatureFactoryOptions, +): TableFeature> { + const { createSignal, createMemo } = factory + return { getDefaultTableOptions(table) { return { @@ -143,77 +142,97 @@ function constructAngularReactivityFeature< } }, constructTableAPIs: (table) => { - const rootNotifier = signal | null>(null) - table.setTableNotifier = (notifier) => rootNotifier.set(notifier) - table.get = computed(() => rootNotifier()!(), { equal: () => false }) - setReactivePropertiesOnObject(table.get, table, { + const rootNotifier = createSignal + > | null>(null) + + table.setTableNotifier = (notifier) => { + rootNotifier.set(notifier) + } + + table.value = () => { + const notifier = rootNotifier() + void notifier?.() + return table as any + } + + setReactivePropertiesOnObject(table.value, table, { overridePrototype: false, skipProperty: skipBaseProperties, + factory, }) }, assignCellPrototype: (prototype, table) => { + // @ts-expect-error Internal if (table.options.reactivity?.cell === false) { return } - setReactivePropertiesOnObject(table.get, prototype, { + // @ts-expect-error Internal + setReactivePropertiesOnObject(table.value, prototype, { skipProperty: getUserSkipPropertyFn( + // @ts-expect-error Internal table.options.reactivity?.cell, skipBaseProperties, ), overridePrototype: true, + factory, }) }, assignColumnPrototype: (prototype, table) => { + // @ts-expect-error Internal if (table.options.reactivity?.column === false) { return } - setReactivePropertiesOnObject(table.get, prototype, { + // @ts-expect-error Internal + setReactivePropertiesOnObject(table.value, prototype, { skipProperty: getUserSkipPropertyFn( + // @ts-expect-error Internal table.options.reactivity?.cell, skipBaseProperties, ), overridePrototype: true, + factory, }) }, assignHeaderPrototype: (prototype, table) => { + // @ts-expect-error Internal if (table.options.reactivity?.header === false) { return } - setReactivePropertiesOnObject(table.get, prototype, { + // @ts-expect-error Internal + setReactivePropertiesOnObject(table.value, prototype, { skipProperty: getUserSkipPropertyFn( + // @ts-expect-error Internal table.options.reactivity?.cell, skipBaseProperties, ), overridePrototype: true, + factory, }) }, assignRowPrototype: (prototype, table) => { + // @ts-expect-error Internal if (table.options.reactivity?.row === false) { return } - setReactivePropertiesOnObject(table.get, prototype, { + // @ts-expect-error Internal + setReactivePropertiesOnObject(table.value, prototype, { skipProperty: getUserSkipPropertyFn( + // @ts-expect-error Internal table.options.reactivity?.cell, skipBaseProperties, ), overridePrototype: true, + factory, }) }, } } -/** - * Angular reactivity feature that add reactive signal supports in table core instance. - * This is used internally by the Angular table adapter `injectTable`. - * - * @private - */ -export const angularReactivityFeature = constructAngularReactivityFeature() - /** * Default predicate used to skip base/non-reactive properties. */ diff --git a/packages/angular-table/src/reactivityUtils.ts b/packages/table-core/src/features/table-reactivity/tableReactivityFeature.utils.ts similarity index 83% rename from packages/angular-table/src/reactivityUtils.ts rename to packages/table-core/src/features/table-reactivity/tableReactivityFeature.utils.ts index 1f64f7355c..43e1efbb20 100644 --- a/packages/angular-table/src/reactivityUtils.ts +++ b/packages/table-core/src/features/table-reactivity/tableReactivityFeature.utils.ts @@ -1,7 +1,6 @@ -import { computed, isSignal } from '@angular/core' -import { $internalMemoFnMeta, getMemoFnMeta } from '@tanstack/table-core' -import type { MemoFnMeta } from '@tanstack/table-core' -import type { Signal } from '@angular/core' +import { $internalMemoFnMeta, getMemoFnMeta } from '../../utils' +import type { MemoFnMeta } from '../../utils' +import type { ReactivityFeatureFactoryOptions } from './tableReactivityFeature' const $TABLE_REACTIVE = Symbol('reactive') @@ -13,6 +12,8 @@ function isReactive(obj: T): boolean { return Reflect.get(obj as {}, $TABLE_REACTIVE) === true } +export type Accessor = () => T + /** * Defines a lazy computed property on an object. The property is initialized * with a getter that computes its value only when accessed for the first time. @@ -22,25 +23,26 @@ function isReactive(obj: T): boolean { * @internal should be used only internally */ export function defineLazyComputedProperty( - notifier: Signal, + notifier: Accessor, setObjectOptions: { originalObject: T property: keyof T & string valueFn: (...args: any) => any overridePrototype?: boolean + factory: ReactivityFeatureFactoryOptions }, ) { - const { originalObject, property, overridePrototype, valueFn } = + const { originalObject, property, overridePrototype, valueFn, factory } = setObjectOptions if (overridePrototype) { - assignReactivePrototypeAPI(notifier, originalObject, property) + assignReactivePrototypeAPI(notifier, originalObject, property, factory) } else { Object.defineProperty(originalObject, property, { enumerable: true, configurable: true, get() { - const computedValue = toComputed(notifier, valueFn, property) + const computedValue = toComputed(notifier, valueFn, property, factory) markReactive(computedValue) // Once the property is set the first time, we don't need a getter anymore // since we have a computed / cached fn value @@ -59,7 +61,7 @@ export function defineLazyComputedProperty( * @internal should be used only internally */ type ComputedFunction = T extends () => infer TReturn - ? Signal + ? Accessor : // 1+ args T extends (arg0?: any, ...args: Array) => any ? T @@ -85,26 +87,25 @@ export function toComputed< TReturn, TFunction extends (...args: any) => TReturn, >( - notifier: Signal, + notifier: Accessor, fn: TFunction, debugName: string, + factory: ReactivityFeatureFactoryOptions, ): ComputedFunction { + const { createMemo } = factory const hasArgs = getFnArgsLength(fn) > 0 if (!hasArgs) { - const computedFn = computed( - () => { - void notifier() - return fn() - }, - { debugName }, - ) + const computedFn = createMemo(() => { + void notifier() + return fn() + }) Object.defineProperty(computedFn, 'name', { value: debugName }) markReactive(computedFn) return computedFn as ComputedFunction } const computedFn: ((this: unknown, ...argsArray: Array) => unknown) & { - _reactiveCache?: Record> + _reactiveCache?: Record> } = function (this: unknown, ...argsArray: Array) { const cacheable = argsArray.length === 0 || @@ -125,13 +126,10 @@ export function toComputed< if ((computedFn._reactiveCache ??= {})[serializedArgs]) { return computedFn._reactiveCache[serializedArgs]() } - const computedSignal = computed( - () => { - void notifier() - return fn.apply(this, argsArray) - }, - { debugName }, - ) + const computedSignal = createMemo(() => { + void notifier() + return fn.apply(this, argsArray) + }) computedFn._reactiveCache[serializedArgs] = computedSignal @@ -155,12 +153,15 @@ function getFnArgsLength( } function assignReactivePrototypeAPI( - notifier: Signal, + notifier: Accessor, prototype: Record, fnName: string, + factory: ReactivityFeatureFactoryOptions, ) { if (isReactive(prototype[fnName])) return + const { createMemo } = factory + const fn = prototype[fnName] const originalArgsLength = getFnArgsLength(fn) @@ -173,13 +174,11 @@ function assignReactivePrototypeAPI( // Create a cache in the current prototype to allow the signals // to be garbage collected. Shorthand for a WeakMap implementation self._reactiveCache ??= {} - const cached = (self._reactiveCache[`${self.id}${fnName}`] ??= computed( - () => { + const cached = (self._reactiveCache[`${self.id}${fnName}`] ??= + createMemo(() => { notifier() return fn.apply(self) - }, - {}, - )) + })) markReactive(cached) cached[$internalMemoFnMeta] = { originalArgsLength, @@ -200,14 +199,15 @@ function assignReactivePrototypeAPI( } export function setReactivePropertiesOnObject( - notifier: Signal, + notifier: Accessor, obj: { [key: string]: any }, options: { overridePrototype?: boolean skipProperty: (property: string) => boolean + factory: ReactivityFeatureFactoryOptions }, ) { - const { skipProperty } = options + const { skipProperty, factory } = options if (isReactive(obj)) { return } @@ -215,11 +215,7 @@ export function setReactivePropertiesOnObject( for (const property in obj) { const value = obj[property] - if ( - isSignal(value) || - typeof value !== 'function' || - skipProperty(property) - ) { + if (typeof value !== 'function' || skipProperty(property)) { continue } defineLazyComputedProperty(notifier, { @@ -227,6 +223,7 @@ export function setReactivePropertiesOnObject( property, originalObject: obj, overridePrototype: options.overridePrototype, + factory: factory, }) } } diff --git a/packages/table-core/src/index.ts b/packages/table-core/src/index.ts index ffab1bfbf2..099953ad94 100755 --- a/packages/table-core/src/index.ts +++ b/packages/table-core/src/index.ts @@ -78,6 +78,10 @@ export * from './fns/sortFns' export * from './features/stockFeatures' +// tableReactivityFeature +export * from './features/table-reactivity/tableReactivityFeature' +export * from './features/table-reactivity/tableReactivityFeature.utils' + // columnFacetingFeature export * from './features/column-faceting/columnFacetingFeature' export * from './features/column-faceting/columnFacetingFeature.types' diff --git a/packages/table-devtools/src/TableContextProvider.tsx b/packages/table-devtools/src/TableContextProvider.tsx index 7a7c1d0f8e..e2d8ad5f21 100644 --- a/packages/table-devtools/src/TableContextProvider.tsx +++ b/packages/table-devtools/src/TableContextProvider.tsx @@ -10,10 +10,15 @@ import { subscribeTableDevtoolsTarget, } from './tableTarget' import type { Accessor, ParentComponent, Setter } from 'solid-js' -import type { RowData, Table, stockFeatures } from '@tanstack/table-core' +import type { RowData, Table, TableFeatures } from '@tanstack/table-core' -export type TableDevtoolsTabId = 'features' | 'state' | 'options' | 'rows' | 'columns' -type AnyTable = Table +export type TableDevtoolsTabId = + | 'features' + | 'state' + | 'options' + | 'rows' + | 'columns' +type AnyTable = Table interface TableDevtoolsContextValue { table: Accessor diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 371e41ebaa..e5499862de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3674,6 +3674,9 @@ importers: '@tanstack/table-core': specifier: workspace:* version: link:../table-core + clsx: + specifier: ^2.1.1 + version: 2.1.1 goober: specifier: ^2.1.18 version: 2.1.18(csstype@3.2.3) From f4d29ceea7f4209a819c1d91ad28ac885050d425 Mon Sep 17 00:00:00 2001 From: Riccardo Perra Date: Tue, 3 Mar 2026 14:07:32 +0100 Subject: [PATCH 4/4] [WIP] feat: refactor reactivity feature with store override (#6182) * feat: update @tanstack/store to v9 * Update packages/table-core/src/core/table/constructTable.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: lockfile * ci: apply automated fixes * feat: add solid table devtools * feat: add table devtools options panel * feat: move reactivity feature to core * feat: refactor reactivity feature with notifier impl * feat: try to refactor vue adapter * ci: apply automated fixes * wip vue adapter * wip options as store + some fixes in solid, angular adapter. fix react adapter * fix vue adapter * fix types * fix table devtools * fix lint * fix table devtools * fix knip issues --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../angular/row-selection/src/app/app.html | 4 + examples/angular/row-selection/src/app/app.ts | 8 +- .../app/selection-column/selection-column.ts | 1 + examples/solid/row-selection/package.json | 4 +- examples/solid/row-selection/src/App.tsx | 11 +- examples/vue/row-selection/src/App.vue | 13 +- knip.json | 5 +- packages/angular-table/src/injectTable.ts | 77 +++-- .../tests/angularReactivityFeature.test.ts | 33 ++- .../flex-render/flex-render-table.test.ts | 2 +- .../angular-table/tests/injectTable.test.ts | 2 +- packages/react-table/src/useTable.ts | 29 +- packages/solid-table-devtools/package.json | 18 +- ...lidTableDevtools.tsx => TableDevtools.tsx} | 4 +- packages/solid-table-devtools/src/index.ts | 21 +- packages/solid-table-devtools/src/plugin.tsx | 4 +- .../solid-table-devtools/src/production.ts | 9 +- .../src/production/TableDevtools.tsx | 10 + .../src/production/plugin.tsx | 9 + packages/solid-table-devtools/vite.config.ts | 22 +- packages/solid-table/src/createTable.ts | 75 +---- .../src/core/table/constructTable.ts | 14 +- .../src/core/table/coreTablesFeature.types.ts | 8 +- .../src/core/table/coreTablesFeature.utils.ts | 14 +- .../tableReactivityFeature.ts | 263 ++---------------- .../tableReactivityFeature.utils.ts | 229 --------------- packages/table-core/src/index.ts | 1 - packages/table-core/src/utils.ts | 2 +- packages/table-devtools/package.json | 1 - .../src/TableContextProvider.tsx | 11 +- .../src/components/FeaturesPanel.tsx | 8 +- .../src/components/OptionsPanel.tsx | 2 +- packages/vue-table/src/createTableHelper.ts | 6 +- packages/vue-table/src/useTable.ts | 105 +++---- pnpm-lock.yaml | 7 +- pnpm-workspace.yaml | 9 +- 36 files changed, 318 insertions(+), 723 deletions(-) rename packages/solid-table-devtools/src/{SolidTableDevtools.tsx => TableDevtools.tsx} (100%) create mode 100644 packages/solid-table-devtools/src/production/TableDevtools.tsx create mode 100644 packages/solid-table-devtools/src/production/plugin.tsx delete mode 100644 packages/table-core/src/features/table-reactivity/tableReactivityFeature.utils.ts diff --git a/examples/angular/row-selection/src/app/app.html b/examples/angular/row-selection/src/app/app.html index a9b5f0461e..35f6a1104b 100644 --- a/examples/angular/row-selection/src/app/app.html +++ b/examples/angular/row-selection/src/app/app.html @@ -1,4 +1,8 @@
+ +
diff --git a/examples/angular/row-selection/src/app/app.ts b/examples/angular/row-selection/src/app/app.ts index 756ab56f20..727c8081ee 100644 --- a/examples/angular/row-selection/src/app/app.ts +++ b/examples/angular/row-selection/src/app/app.ts @@ -32,6 +32,7 @@ export class App { private readonly rowSelection = signal({}) readonly globalFilter = signal('') readonly data = signal(makeData(10_000)) + readonly enableRowSelection = signal(true) readonly columns = columnHelper.columns([ columnHelper.display({ @@ -93,7 +94,8 @@ export class App { state: { rowSelection: this.rowSelection(), }, - enableRowSelection: true, // enable row selection for all rows + + enableRowSelection: this.enableRowSelection(), // enable row selection for all rows // enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row onRowSelectionChange: (updaterOrValue) => { this.rowSelection.set( @@ -136,4 +138,8 @@ export class App { refreshData(): void { this.data.set(makeData(10_000)) } + + toggleEnableRowSelection() { + this.enableRowSelection.update((value) => !value) + } } diff --git a/examples/angular/row-selection/src/app/selection-column/selection-column.ts b/examples/angular/row-selection/src/app/selection-column/selection-column.ts index 5be2238e13..a063cbef41 100644 --- a/examples/angular/row-selection/src/app/selection-column/selection-column.ts +++ b/examples/angular/row-selection/src/app/selection-column/selection-column.ts @@ -29,6 +29,7 @@ export class TableHeaderSelection { `, diff --git a/examples/solid/row-selection/package.json b/examples/solid/row-selection/package.json index 8fdf7ab281..8ad3256001 100644 --- a/examples/solid/row-selection/package.json +++ b/examples/solid/row-selection/package.json @@ -17,9 +17,9 @@ "vite-plugin-solid": "^2.11.10" }, "dependencies": { - "@tanstack/solid-table": "^9.0.0-alpha.10", "@tanstack/solid-devtools": "^0.7.26", - "@tanstack/solid-table-devtools": "9.0.0-alpha.11", + "@tanstack/solid-table": "^9.0.0-alpha.10", + "@tanstack/solid-table-devtools": "workspace:*", "solid-js": "^1.9.11" } } diff --git a/examples/solid/row-selection/src/App.tsx b/examples/solid/row-selection/src/App.tsx index 7e22049531..93efd2841a 100644 --- a/examples/solid/row-selection/src/App.tsx +++ b/examples/solid/row-selection/src/App.tsx @@ -33,6 +33,7 @@ export const _features = tableFeatures({ function App() { const [data, setData] = createSignal(makeData(1_000)) const refreshData = () => setData(makeData(100_000)) // stress test + const [enableRowSelection, setEnableRowSelection] = createSignal(true) // Create table first with a placeholder for columns let table: SolidTable @@ -117,8 +118,6 @@ function App() { }, ] - const [enableRowSelection, setEnableRowSelection] = createSignal(true) - table = createTable({ _features, _rowModels: { @@ -136,8 +135,6 @@ function App() { debugTable: true, }) - window.setEnable = setEnableRowSelection - return ( // ({ @@ -336,6 +333,12 @@ function App() { > Log table.getSelectedRowModel().flatRows +
diff --git a/examples/vue/row-selection/src/App.vue b/examples/vue/row-selection/src/App.vue index 58501f58c8..791e73ec67 100644 --- a/examples/vue/row-selection/src/App.vue +++ b/examples/vue/row-selection/src/App.vue @@ -88,18 +88,26 @@ const columns = columnHelper.columns([ ]) const data = ref(makeData(10)) +const enableRowSelection = ref(true) const rerender = () => { data.value = makeData(10) } +const toggleRowSelection = () => { + enableRowSelection.value = !enableRowSelection.value +} + const table = useTable( { _features, _rowModels: {}, data, columns, - enableRowSelection: true, //enable row selection for all rows + // enable row selection for all rows + get enableRowSelection() { + return enableRowSelection.value + }, // enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row }, (state) => ({ rowSelection: state.rowSelection }), @@ -152,6 +160,9 @@ const table = useTable(
+
diff --git a/knip.json b/knip.json index d3668b30bf..4c38f1ee81 100644 --- a/knip.json +++ b/knip.json @@ -1,9 +1,12 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", "ignoreDependencies": ["@faker-js/faker"], - "ignoreWorkspaces": ["examples/**", "packages/table-core/tests/**"], + "ignoreWorkspaces": ["examples/**"], "ignore": ["**/*benchmark*", "**/benchmarks/**"], "workspaces": { + "packages/table-core": { + "ignore": ["**/tests/**"] + }, "packages/match-sorter-utils": { "ignoreDependencies": ["remove-accents"] }, diff --git a/packages/angular-table/src/injectTable.ts b/packages/angular-table/src/injectTable.ts index fec962fad1..0f9b53a4c1 100644 --- a/packages/angular-table/src/injectTable.ts +++ b/packages/angular-table/src/injectTable.ts @@ -2,8 +2,8 @@ import { Injector, assertInInjectionContext, computed, + effect, inject, - isSignal, signal, untracked, } from '@angular/core' @@ -31,6 +31,10 @@ export type AngularTable< * The selected state from the table store, based on the selector provided. */ readonly state: Signal> + /** + * A signal that returns the entire table instance. Will update on table/options change. + */ + readonly value: Signal> /** * Subscribe to changes in the table store with a custom selector. */ @@ -107,18 +111,18 @@ export function injectTable< ): AngularTable { assertInInjectionContext(injectTable) const injector = inject(Injector) - - const angularReactivityFeature = constructReactivityFeature({ - createSignal: (value) => { - return signal(value) as any - }, - createMemo: (fn) => { - return computed(() => fn()) - }, - isSignal: (value) => isSignal(value), - }) + const count = 0 return lazyInit(() => { + const stateNotifier = signal(0) + + const angularReactivityFeature = constructReactivityFeature({ + // optionsNotifier: () => stateNotifier(), + stateNotifier: () => { + return stateNotifier() + }, + }) + const resolvedOptions: TableOptions = { ...options(), _features: { @@ -127,14 +131,17 @@ export function injectTable< }, } as TableOptions - const table: AngularTable = constructTable( - resolvedOptions, - ) as AngularTable + const table = constructTable(resolvedOptions) as AngularTable< + TFeatures, + TData, + TSelected + > const updatedOptions = computed>(() => { const tableOptionsValue = options() + const currentOptions = table.latestOptions const result: TableOptions = { - ...table.options, + ...currentOptions, ...tableOptionsValue, _features: { ...tableOptionsValue._features, @@ -147,26 +154,34 @@ export function injectTable< return result }) - const tableState = injectStore( - table.store, - (state: TableState) => state, + effect( + () => { + const newOptions = updatedOptions() + untracked(() => table.setOptions(newOptions)) + }, { injector }, ) - const tableSignalNotifier = computed( + const tableState = injectStore(table.store, (state) => state, { injector }) + const tableOptions = injectStore(table.optionsStore, (state) => state, { + injector, + }) + + let firstRun = true + effect( () => { + tableOptions() tableState() - const newOptions = updatedOptions() - untracked(() => table.setOptions(newOptions)) - untracked(() => table.baseStore.setState((prev) => ({ ...prev }))) - return table + if (!firstRun) { + untracked(() => { + stateNotifier.update((n) => n + 1) + }) + } + firstRun = false }, - { equal: () => false }, + { injector }, ) - // @ts-ignore - table.setTableNotifier(tableSignalNotifier) - table.Subscribe = function Subscribe(props: { selector: (state: TableState) => TSubSelected equal?: ValueEqualityFn @@ -176,11 +191,17 @@ export function injectTable< equal: props.equal, }) } - Object.defineProperty(table, 'state', { value: injectStore(table.store, selector, { injector }), }) + Object.defineProperty(table, 'value', { + value: computed(() => { + stateNotifier() + return table + }), + }) + return table }) } diff --git a/packages/angular-table/tests/angularReactivityFeature.test.ts b/packages/angular-table/tests/angularReactivityFeature.test.ts index f2de587be1..d6626b7915 100644 --- a/packages/angular-table/tests/angularReactivityFeature.test.ts +++ b/packages/angular-table/tests/angularReactivityFeature.test.ts @@ -44,7 +44,7 @@ describe('angularReactivityFeature', () => { const table = createTestTable() const tablePropertyKeys = Object.keys(table) - describe('Table property reactivity', () => { + describe.skip('Table property reactivity', () => { test.each( tablePropertyKeys.map((property) => [ property, @@ -74,7 +74,7 @@ describe('angularReactivityFeature', () => { }) }) - describe('Header property reactivity', () => { + describe.skip('Header property reactivity', () => { const headers = table.getHeaderGroups() headers.forEach((headerGroup, index) => { const headerPropertyKeys = Object.keys(headerGroup) @@ -112,7 +112,7 @@ describe('angularReactivityFeature', () => { }) }) - describe('Column property reactivity', () => { + describe.skip('Column property reactivity', () => { const columns = table.getAllColumns() columns.forEach((column, index) => { const columnPropertyKeys = Object.keys(column).concat( @@ -133,7 +133,7 @@ describe('angularReactivityFeature', () => { }) }) - describe('Row and cells property reactivity', () => { + describe.skip('Row and cells property reactivity', () => { const flatRows = table.getRowModel().flatRows flatRows.forEach((row, index) => { const rowsPropertyKeys = Object.keys(row).concat( @@ -174,11 +174,12 @@ describe('angularReactivityFeature', () => { }) describe('Integration', () => { - test('methods works will be reactive effects', () => { + test('methods within effect will be re-trigger when options/state changes', () => { const data = signal>([{ id: '1', title: 'Title' }]) const table = createTestTable(data) const isSelectedRow1Captor = vi.fn<(val: boolean) => void>() const cellGetValueCaptor = vi.fn<(val: unknown) => void>() + const cellGetValueMemoizedCaptor = vi.fn<(val: unknown) => void>() const columnIsVisibleCaptor = vi.fn<(val: boolean) => void>() // This will test a case where you put in the effect a single cell property method @@ -188,6 +189,8 @@ describe('angularReactivityFeature', () => { () => table.getRowModel().rows[0]!.getAllCells()[0]!, ) + const cellGetValue = computed(() => cell().getValue()) + TestBed.runInInjectionContext(() => { effect(() => { isSelectedRow1Captor(cell().row.getIsSelected()) @@ -195,6 +198,9 @@ describe('angularReactivityFeature', () => { effect(() => { cellGetValueCaptor(cell().getValue()) }) + effect(() => { + cellGetValueMemoizedCaptor(cellGetValue()) + }) effect(() => { columnIsVisibleCaptor(cell().column.getIsVisible()) }) @@ -202,6 +208,7 @@ describe('angularReactivityFeature', () => { TestBed.tick() expect(isSelectedRow1Captor).toHaveBeenCalledTimes(1) + expect(cellGetValueMemoizedCaptor).toHaveBeenCalledTimes(1) expect(cellGetValueCaptor).toHaveBeenCalledTimes(1) expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(1) @@ -209,7 +216,7 @@ describe('angularReactivityFeature', () => { TestBed.tick() expect(isSelectedRow1Captor).toHaveBeenCalledTimes(2) expect(cellGetValueCaptor).toHaveBeenCalledTimes(1) - expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(1) + expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(2) data.set([{ id: '1', title: 'Title 3' }]) TestBed.tick() @@ -217,17 +224,23 @@ describe('angularReactivityFeature', () => { // the cell instance will be recreated expect(isSelectedRow1Captor).toHaveBeenCalledTimes(3) expect(cellGetValueCaptor).toHaveBeenCalledTimes(2) - expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(2) + expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(3) cell().column.toggleVisibility(false) TestBed.tick() - expect(isSelectedRow1Captor).toHaveBeenCalledTimes(3) + expect(isSelectedRow1Captor).toHaveBeenCalledTimes(4) expect(cellGetValueCaptor).toHaveBeenCalledTimes(2) - expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(3) + expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(4) - expect(isSelectedRow1Captor.mock.calls).toEqual([[false], [true], [true]]) + expect(isSelectedRow1Captor.mock.calls).toEqual([ + [false], + [true], + [true], + [true], + ]) expect(cellGetValueCaptor.mock.calls).toEqual([['1'], ['1']]) expect(columnIsVisibleCaptor.mock.calls).toEqual([ + [true], [true], [true], [false], diff --git a/packages/angular-table/tests/flex-render/flex-render-table.test.ts b/packages/angular-table/tests/flex-render/flex-render-table.test.ts index 4781734d6c..3312226c78 100644 --- a/packages/angular-table/tests/flex-render/flex-render-table.test.ts +++ b/packages/angular-table/tests/flex-render/flex-render-table.test.ts @@ -319,7 +319,7 @@ describe('FlexRenderDirective', () => { callExpandRender() return flexRenderComponent(ExpandCell, { bindings: [ - inputBinding('expanded', row.getIsExpanded), + inputBinding('expanded', () => row.getIsExpanded()), outputBinding('toggleExpand', () => row.toggleExpanded()), ], }) diff --git a/packages/angular-table/tests/injectTable.test.ts b/packages/angular-table/tests/injectTable.test.ts index e2700dcb8a..bd6e4d17e0 100644 --- a/packages/angular-table/tests/injectTable.test.ts +++ b/packages/angular-table/tests/injectTable.test.ts @@ -75,7 +75,7 @@ describe('injectTable', () => { }) test('supports "Object.keys"', () => { - const keys = Object.keys(table.value()).concat('state') + const keys = Object.keys(table.value()).concat('state', 'value') expect(Object.keys(table)).toEqual(keys) }) diff --git a/packages/react-table/src/useTable.ts b/packages/react-table/src/useTable.ts index 60d0685b0b..4f1deb83a0 100644 --- a/packages/react-table/src/useTable.ts +++ b/packages/react-table/src/useTable.ts @@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useMemo, useState } from 'react' import { constructTable } from '@tanstack/table-core' -import { useStore } from '@tanstack/react-store' +import { shallow, useStore } from '@tanstack/react-store' import { FlexRender } from './FlexRender' import { Subscribe } from './Subscribe' import type { @@ -103,26 +103,15 @@ export function useTable< return tableInstance }) - // sync table options on every render - table.setOptions((prev) => ({ - ...prev, - ...tableOptions, - })) + useEffect(() => { + // sync table options on every render + table.setOptions((prev) => ({ + ...prev, + ...tableOptions, + })) + }, [table, tableOptions]) - useIsomorphicLayoutEffect(() => { - // prevent race condition between table.setOptions and table.baseStore.setState - queueMicrotask(() => { - table.baseStore.setState((prev) => ({ - ...prev, - })) - }) - }, [ - table.options.columns, // re-render when columns change - table.options.data, // re-render when data changes - table.options.state, // sync react state to the table store - ]) - - const state = useStore(table.store, selector) + const state = useStore(table.store, selector, shallow) return useMemo( () => ({ diff --git a/packages/solid-table-devtools/package.json b/packages/solid-table-devtools/package.json index eea909e0ab..b8bfaf0765 100644 --- a/packages/solid-table-devtools/package.json +++ b/packages/solid-table-devtools/package.json @@ -27,22 +27,22 @@ "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc", "test:build": "publint --strict", - "build": "tsc -p tsconfig.build.json" + "build": "vite build" }, "type": "module", - "types": "dist/index.d.ts", - "module": "dist/index.js", + "types": "dist/esm/index.d.ts", + "module": "dist/esm/index.js", "exports": { ".": { "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" } }, "./production": { "import": { - "types": "./dist/production.d.ts", - "default": "./dist/production.js" + "types": "./dist/esm/production.d.ts", + "default": "./dist/esm/production.js" } }, "./package.json": "./package.json" @@ -60,8 +60,10 @@ "@tanstack/table-core": "workspace:*", "@tanstack/table-devtools": "workspace:*" }, + "peerDependencies": { + "solid-js": "^1.9.11" + }, "devDependencies": { - "solid-js": "^1.9.11", "vite-plugin-solid": "^2.11.10" } } diff --git a/packages/solid-table-devtools/src/SolidTableDevtools.tsx b/packages/solid-table-devtools/src/TableDevtools.tsx similarity index 100% rename from packages/solid-table-devtools/src/SolidTableDevtools.tsx rename to packages/solid-table-devtools/src/TableDevtools.tsx index 2add8e7819..29c1a28712 100644 --- a/packages/solid-table-devtools/src/SolidTableDevtools.tsx +++ b/packages/solid-table-devtools/src/TableDevtools.tsx @@ -1,9 +1,9 @@ -import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/solid' import { createSolidPanel } from '@tanstack/devtools-utils/solid' import { - setTableDevtoolsTarget, TableDevtoolsCore, + setTableDevtoolsTarget, } from '@tanstack/table-devtools' +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/solid' import type { RowData, Table, TableFeatures } from '@tanstack/table-core' diff --git a/packages/solid-table-devtools/src/index.ts b/packages/solid-table-devtools/src/index.ts index c34bc8c874..70cdd3bcf1 100644 --- a/packages/solid-table-devtools/src/index.ts +++ b/packages/solid-table-devtools/src/index.ts @@ -1,17 +1,14 @@ -'use client' - -import * as Devtools from './SolidTableDevtools' +import { isDev } from 'solid-js/web' import * as plugin from './plugin' +import * as Devtools from './TableDevtools' -export const TableDevtoolsPanel = - process.env.NODE_ENV !== 'development' - ? Devtools.TableDevtoolsPanelNoOp - : Devtools.TableDevtoolsPanel +export const TableDevtoolsPanel = !isDev + ? Devtools.TableDevtoolsPanelNoOp + : Devtools.TableDevtoolsPanel -export const tableDevtoolsPlugin = - process.env.NODE_ENV !== 'development' - ? plugin.tableDevtoolsNoOpPlugin - : plugin.tableDevtoolsPlugin +export const tableDevtoolsPlugin = !isDev + ? plugin.tableDevtoolsNoOpPlugin + : plugin.tableDevtoolsPlugin -export type { TableDevtoolsSolidInit } from './SolidTableDevtools' +export type { TableDevtoolsSolidInit } from './TableDevtools' export type { TableDevtoolsPluginOptions } from './plugin' diff --git a/packages/solid-table-devtools/src/plugin.tsx b/packages/solid-table-devtools/src/plugin.tsx index dfc00d2760..07561b9b35 100644 --- a/packages/solid-table-devtools/src/plugin.tsx +++ b/packages/solid-table-devtools/src/plugin.tsx @@ -1,5 +1,5 @@ -import React from 'react' -import { TableDevtoolsPanel } from './SolidTableDevtools' +import { createSolidPlugin } from '@tanstack/devtools-utils/solid' +import { TableDevtoolsPanel } from './TableDevtools' import type { RowData, Table, TableFeatures } from '@tanstack/table-core' export interface TableDevtoolsPluginOptions< diff --git a/packages/solid-table-devtools/src/production.ts b/packages/solid-table-devtools/src/production.ts index b197295f30..f683bce6b7 100644 --- a/packages/solid-table-devtools/src/production.ts +++ b/packages/solid-table-devtools/src/production.ts @@ -1,6 +1,5 @@ -'use client' +export { TableDevtoolsPanel } from './TableDevtools' -export { TableDevtoolsPanel } from './SolidTableDevtools' -export type { TableDevtoolsSolidInit } from './SolidTableDevtools' -export { tableDevtoolsPlugin } from './plugin' -export type { TableDevtoolsPluginOptions } from './plugin' +export type { TableDevtoolsSolidInit } from './production/TableDevtools' + +export { tableDevtoolsPlugin } from './production/plugin' diff --git a/packages/solid-table-devtools/src/production/TableDevtools.tsx b/packages/solid-table-devtools/src/production/TableDevtools.tsx new file mode 100644 index 0000000000..f0a5ef6f4c --- /dev/null +++ b/packages/solid-table-devtools/src/production/TableDevtools.tsx @@ -0,0 +1,10 @@ +import { createSolidPanel } from '@tanstack/devtools-utils/solid' +import { TableDevtoolsCore } from '@tanstack/table-devtools/production' + +import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/solid' + +const [TableDevtoolsPanel] = createSolidPanel(TableDevtoolsCore) + +export interface TableDevtoolsSolidInit extends DevtoolsPanelProps {} + +export { TableDevtoolsPanel } diff --git a/packages/solid-table-devtools/src/production/plugin.tsx b/packages/solid-table-devtools/src/production/plugin.tsx new file mode 100644 index 0000000000..403eedabc3 --- /dev/null +++ b/packages/solid-table-devtools/src/production/plugin.tsx @@ -0,0 +1,9 @@ +import { createSolidPlugin } from '@tanstack/devtools-utils/solid' +import { TableDevtoolsPanel } from './TableDevtools' + +const [tableDevtoolsPlugin] = createSolidPlugin({ + name: 'TanStack Table', + Component: TableDevtoolsPanel, +}) + +export { tableDevtoolsPlugin } diff --git a/packages/solid-table-devtools/vite.config.ts b/packages/solid-table-devtools/vite.config.ts index a872ffe359..896c24e885 100644 --- a/packages/solid-table-devtools/vite.config.ts +++ b/packages/solid-table-devtools/vite.config.ts @@ -1,6 +1,24 @@ -import { defineConfig } from 'vitest/config' +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/vite-config' import solid from 'vite-plugin-solid' +import packageJson from './package.json' -export default defineConfig({ +const config = defineConfig({ plugins: [solid()], + test: { + name: packageJson.name, + dir: './', + watch: false, + environment: 'jsdom', + globals: true, + }, }) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: ['./src/index.ts', './src/production.ts'], + srcDir: './src', + cjs: false, + }), +) diff --git a/packages/solid-table/src/createTable.ts b/packages/solid-table/src/createTable.ts index 13f68366be..b4a9658357 100644 --- a/packages/solid-table/src/createTable.ts +++ b/packages/solid-table/src/createTable.ts @@ -3,14 +3,7 @@ import { constructTable, } from '@tanstack/table-core' import { useStore } from '@tanstack/solid-store' -import { - createComputed, - createMemo, - createSignal, - getOwner, - mergeProps, - runWithOwner, -} from 'solid-js' +import { createComputed, createSignal, mergeProps } from 'solid-js' import type { Accessor, JSX } from 'solid-js' import type { NoInfer, @@ -64,25 +57,14 @@ export function createTable< selector: (state: TableState) => TSelected = () => ({}) as TSelected, ): SolidTable { - const owner = getOwner() + const [notifier, setNotifier] = createSignal(void 0, { equals: false }) + const [optionsNotifier, setOptionsNotifier] = createSignal(void 0, { + equals: false, + }) const solidReactivityFeature = constructReactivityFeature({ - createSignal: (value) => { - const signal = createSignal(value) - function interoperableSignal() { - return signal[0]() - } - return Object.assign(interoperableSignal, { - set: (value: any) => signal[1](() => value), - }) - }, - createMemo: (fn) => { - if (owner) { - return runWithOwner(owner, () => createMemo(fn))! - } - return createMemo(fn) - }, - isSignal: (value) => typeof value === 'function', + stateNotifier: () => notifier(), + optionsNotifier: () => optionsNotifier(), }) const mergedOptions = mergeProps(tableOptions, { @@ -91,8 +73,6 @@ export function createTable< }), }) as any - const [renderVersion, setRenderVersion] = createSignal(0) - const resolvedOptions = mergeProps( { mergeOptions: ( @@ -111,17 +91,8 @@ export function createTable< TSelected > - // @ts-ignore - table.setTableNotifier(() => { - renderVersion() - return table - }) - - /** - * Temp force reactivity to all state changes on every table.get* method - */ const allState = useStore(table.store, (state) => state) - const allOptions = useStore(table.baseOptionsStore, (options) => options) + const allOptions = useStore(table.optionsStore, (options) => options) createComputed(() => { table.setOptions((prev) => { @@ -130,39 +101,11 @@ export function createTable< }) createComputed(() => { - // Access storeState to create reactive dependency allState() allOptions() - // Increment version to invalidate cached get* methods - setRenderVersion((v) => v + 1) - // Update options when store changes - // table.setOptions((prev) => { - // return mergeProps(prev, tableOptions) as TableOptions - // }) + setNotifier(void 0) }) - // Object.assign(table, { - // get options() { - // allOptions() - // return table.baseOptionsStore.get() - // }, - // }) - // - // Object.defineProperty(table.store, 'get', { - // value: () => { - // allState() - // allOptions() - // return table.store['atom'].get() - // }, - // }) - // Object.defineProperty(table.store, 'state', { - // get() { - // allState() - // allOptions() - // return this['atom'].get() - // }, - // }) - table.Subscribe = function Subscribe(props: { selector: (state: TableState) => TSelected children: ((state: Accessor) => JSX.Element) | JSX.Element diff --git a/packages/table-core/src/core/table/constructTable.ts b/packages/table-core/src/core/table/constructTable.ts index 0b94cc6f85..35f441c451 100644 --- a/packages/table-core/src/core/table/constructTable.ts +++ b/packages/table-core/src/core/table/constructTable.ts @@ -42,16 +42,20 @@ export function constructTable< return Object.assign(obj, feature.getDefaultTableOptions?.(table)) }, {}) as TableOptions - table.baseOptionsStore = createStore({ + table.latestOptions = { ...defaultOptions, ...tableOptions, - }) - + } + table.optionsStore = createStore(table.latestOptions) Object.defineProperty(table, 'options', { enumerable: true, configurable: true, get() { - return table.baseOptionsStore.state + return table.optionsStore.state + }, + set(value) { + table.latestOptions = value + table.optionsStore.setState(() => value) }, }) @@ -66,6 +70,8 @@ export function constructTable< const state = table.baseStore.state return { ...state, + // Merge state with non-reactive options. + // NOTE: Adapters should call `table.baseStore.setState` to trigger state updates ...(table.options.state ?? {}), } }) diff --git a/packages/table-core/src/core/table/coreTablesFeature.types.ts b/packages/table-core/src/core/table/coreTablesFeature.types.ts index ccb1224ec1..5b692c740f 100644 --- a/packages/table-core/src/core/table/coreTablesFeature.types.ts +++ b/packages/table-core/src/core/table/coreTablesFeature.types.ts @@ -95,13 +95,17 @@ export interface Table_CoreProperties< */ baseStore: Store> /** - * The base store for the table. This can be used to write to the table state. + * The base store for the table options. */ - baseOptionsStore: Store> + optionsStore: Store> /** * This is the resolved initial state of the table. */ initialState: TableState + /** + * The latest table options value (non reactive). + */ + latestOptions: TableOptions /** * A read-only reference to the table's current options. */ diff --git a/packages/table-core/src/core/table/coreTablesFeature.utils.ts b/packages/table-core/src/core/table/coreTablesFeature.utils.ts index 436a5f1e88..d897137513 100644 --- a/packages/table-core/src/core/table/coreTablesFeature.utils.ts +++ b/packages/table-core/src/core/table/coreTablesFeature.utils.ts @@ -18,12 +18,12 @@ export function table_mergeOptions< table: Table_Internal, newOptions: TableOptions, ) { - if (table.options.mergeOptions) { - return table.options.mergeOptions(table.options, newOptions) + if (table.latestOptions.mergeOptions) { + return table.latestOptions.mergeOptions(table.latestOptions, newOptions) } return { - ...table.options, + ...table.latestOptions, ...newOptions, } } @@ -35,8 +35,8 @@ export function table_setOptions< table: Table_Internal, updater: Updater>, ): void { - table.baseOptionsStore.setState((options) => { - const newOptions = functionalUpdate(updater, options) - return table_mergeOptions(table, newOptions) - }) + const newOptions = functionalUpdate(updater, table.latestOptions) + const mergedOptions = table_mergeOptions(table, newOptions) + table.latestOptions = mergedOptions + table.optionsStore.setState(() => mergedOptions) } diff --git a/packages/table-core/src/features/table-reactivity/tableReactivityFeature.ts b/packages/table-core/src/features/table-reactivity/tableReactivityFeature.ts index 230affbf23..fc63235138 100644 --- a/packages/table-core/src/features/table-reactivity/tableReactivityFeature.ts +++ b/packages/table-core/src/features/table-reactivity/tableReactivityFeature.ts @@ -1,250 +1,47 @@ -import { setReactivePropertiesOnObject } from './tableReactivityFeature.utils' import type { TableFeature, TableFeatures } from '../../types/TableFeatures' -import type { Accessor } from './tableReactivityFeature.utils' import type { RowData } from '../../types/type-utils' -import type { Table } from '../../types/Table' +import type { ReadonlyStore, Store } from '@tanstack/store' -/** - * Predicate used to skip/ignore a property name when applying Angular reactivity. - * - * Returning `true` means the property should NOT be wrapped/made reactive. - */ -type SkipPropertyFn = (property: string) => boolean - -/** - * Fine-grained configuration for Angular reactivity. - * - * Each key controls whether prototype methods/getters on the corresponding TanStack Table - * objects are wrapped with signal-aware access. - * - * - `true` enables wrapping using the default skip rules. - * - `false` disables wrapping entirely for that object type. - * - a function allows customizing the skip rules (see {@link SkipPropertyFn}). - * - * @example - * ```ts - * const table = injectTable(() => { - * // ...table options, - * reactivity: { - * // fine-grained control over which table objects have reactive properties, - * // and which properties are wrapped - * header: true, - * column: true, - * row: true, - * cell: true, - * } - * }) - * ``` - */ -export interface AngularReactivityFlags { - /** Controls reactive wrapping for `Header` instances. */ - header: boolean | SkipPropertyFn - /** Controls reactive wrapping for `Column` instances. */ - column: boolean | SkipPropertyFn - /** Controls reactive wrapping for `Row` instances. */ - row: boolean | SkipPropertyFn - /** Controls reactive wrapping for `Cell` instances. */ - cell: boolean | SkipPropertyFn -} - -/** - * Table option extension for Angular reactivity. - * - * Available on `createTable` options via module augmentation in this file. - */ -interface TableOptions_Reactivity { - /** - * Enables/disables and configures Angular reactivity on table-related prototypes. - * - * If omitted, defaults are provided by the feature. - */ - reactivity?: Partial -} - -/** - * Table API extension for Angular reactivity. - * - * Added to the table instance via module augmentation. - */ -interface Table_Reactivity< - TFeatures extends TableFeatures, - TData extends RowData, -> { - /** - * Returns a table signal that updates whenever the table state or options changes. - */ - value: Accessor> - /** - * Sets the reactive notifier that powers {@link get}. - * - * @internal Used by the Angular table adapter to connect its notifier to the core table. - */ - setTableNotifier: (signal: Accessor>) => void -} - -/** - * Type map describing what this feature adds to TanStack Table constructors. - */ interface TableReactivityFeatureConstructors< TFeatures extends TableFeatures, TData extends RowData, -> { - TableOptions: TableOptions_Reactivity - Table: Table_Reactivity -} - -/** - * Resolves the user-provided `reactivity.*` config to a skip predicate. - * - * - `false` is handled by callers (feature method returns early) - * - `true` selects the default predicate - * - a function overrides the default predicate - */ -const getUserSkipPropertyFn = ( - value: undefined | null | boolean | SkipPropertyFn, - defaultPropertyFn: SkipPropertyFn, -) => { - if (typeof value === 'boolean') { - return defaultPropertyFn - } - - return value ?? defaultPropertyFn -} - -export type InteroperableWritableSignal = { - (): T - set: (value: unknown) => void -} - -export interface ReactivityFeatureFactoryOptions { - createSignal: (value: T) => InteroperableWritableSignal - createMemo: (accessor: Accessor) => Accessor - isSignal: (v: unknown) => boolean -} +> {} export function constructReactivityFeature< TFeatures extends TableFeatures, TData extends RowData, ->( - factory: ReactivityFeatureFactoryOptions, -): TableFeature> { - const { createSignal, createMemo } = factory - +>(bindings: { + stateNotifier?: () => unknown + optionsNotifier?: () => unknown +}): TableFeature> { return { - getDefaultTableOptions(table) { - return { - reactivity: { - header: true, - column: true, - row: true, - cell: true, - }, - } - }, constructTableAPIs: (table) => { - const rootNotifier = createSignal - > | null>(null) - - table.setTableNotifier = (notifier) => { - rootNotifier.set(notifier) - } - - table.value = () => { - const notifier = rootNotifier() - void notifier?.() - return table as any - } - - setReactivePropertiesOnObject(table.value, table, { - overridePrototype: false, - skipProperty: skipBaseProperties, - factory, - }) - }, - - assignCellPrototype: (prototype, table) => { - // @ts-expect-error Internal - if (table.options.reactivity?.cell === false) { - return - } - // @ts-expect-error Internal - setReactivePropertiesOnObject(table.value, prototype, { - skipProperty: getUserSkipPropertyFn( - // @ts-expect-error Internal - table.options.reactivity?.cell, - skipBaseProperties, - ), - overridePrototype: true, - factory, - }) - }, - - assignColumnPrototype: (prototype, table) => { - // @ts-expect-error Internal - if (table.options.reactivity?.column === false) { - return - } - // @ts-expect-error Internal - setReactivePropertiesOnObject(table.value, prototype, { - skipProperty: getUserSkipPropertyFn( - // @ts-expect-error Internal - table.options.reactivity?.cell, - skipBaseProperties, - ), - overridePrototype: true, - factory, - }) - }, - - assignHeaderPrototype: (prototype, table) => { - // @ts-expect-error Internal - if (table.options.reactivity?.header === false) { - return - } - // @ts-expect-error Internal - setReactivePropertiesOnObject(table.value, prototype, { - skipProperty: getUserSkipPropertyFn( - // @ts-expect-error Internal - table.options.reactivity?.cell, - skipBaseProperties, - ), - overridePrototype: true, - factory, - }) - }, - - assignRowPrototype: (prototype, table) => { - // @ts-expect-error Internal - if (table.options.reactivity?.row === false) { - return - } - // @ts-expect-error Internal - setReactivePropertiesOnObject(table.value, prototype, { - skipProperty: getUserSkipPropertyFn( - // @ts-expect-error Internal - table.options.reactivity?.cell, - skipBaseProperties, - ), - overridePrototype: true, - factory, - }) + table.store = bindStore(table.store, bindings.stateNotifier) + table.optionsStore = bindStore( + table.optionsStore, + bindings.optionsNotifier, + ) }, } } -/** - * Default predicate used to skip base/non-reactive properties. - */ -function skipBaseProperties(property: string): boolean { - return ( - // equals `getContext` - property === 'getContext' || - // start with `_` - property[0] === '_' || - // doesn't start with `get`, but faster - !(property[0] === 'g' && property[1] === 'e' && property[2] === 't') || - // ends with `Handler` - property.endsWith('Handler') - ) +const bindStore = | ReadonlyStore>( + store: T, + notifier?: () => unknown, +): T => { + const stateDescriptor = Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(store), + 'state', + )! + + Object.defineProperty(store, 'state', { + configurable: true, + enumerable: true, + get() { + notifier?.() + return stateDescriptor.get!.call(store) + }, + }) + + return store } diff --git a/packages/table-core/src/features/table-reactivity/tableReactivityFeature.utils.ts b/packages/table-core/src/features/table-reactivity/tableReactivityFeature.utils.ts deleted file mode 100644 index 43e1efbb20..0000000000 --- a/packages/table-core/src/features/table-reactivity/tableReactivityFeature.utils.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { $internalMemoFnMeta, getMemoFnMeta } from '../../utils' -import type { MemoFnMeta } from '../../utils' -import type { ReactivityFeatureFactoryOptions } from './tableReactivityFeature' - -const $TABLE_REACTIVE = Symbol('reactive') - -function markReactive(obj: T): void { - Object.defineProperty(obj, $TABLE_REACTIVE, { value: true }) -} - -function isReactive(obj: T): boolean { - return Reflect.get(obj as {}, $TABLE_REACTIVE) === true -} - -export type Accessor = () => T - -/** - * Defines a lazy computed property on an object. The property is initialized - * with a getter that computes its value only when accessed for the first time. - * After the first access, the computed value is cached, and the getter is - * replaced with a direct property assignment for efficiency. - * - * @internal should be used only internally - */ -export function defineLazyComputedProperty( - notifier: Accessor, - setObjectOptions: { - originalObject: T - property: keyof T & string - valueFn: (...args: any) => any - overridePrototype?: boolean - factory: ReactivityFeatureFactoryOptions - }, -) { - const { originalObject, property, overridePrototype, valueFn, factory } = - setObjectOptions - - if (overridePrototype) { - assignReactivePrototypeAPI(notifier, originalObject, property, factory) - } else { - Object.defineProperty(originalObject, property, { - enumerable: true, - configurable: true, - get() { - const computedValue = toComputed(notifier, valueFn, property, factory) - markReactive(computedValue) - // Once the property is set the first time, we don't need a getter anymore - // since we have a computed / cached fn value - Object.defineProperty(originalObject, property, { - value: computedValue, - configurable: true, - enumerable: true, - }) - return computedValue - }, - }) - } -} - -/** - * @internal should be used only internally - */ -type ComputedFunction = T extends () => infer TReturn - ? Accessor - : // 1+ args - T extends (arg0?: any, ...args: Array) => any - ? T - : never - -/** - * @description Transform a function into a computed that react to given notifier re-computations - * - * Here we'll handle all type of accessors: - * - 0 argument -> e.g. table.getCanNextPage()) - * - 0~1 arguments -> e.g. table.getIsSomeRowsPinned(position?) - * - 1 required argument -> e.g. table.getColumn(columnId) - * - 1+ argument -> e.g. table.getRow(id, searchAll?) - * - * Since we are not able to detect automatically the accessors parameters, - * we'll wrap all accessors into a cached function wrapping a computed - * that return it's value based on the given parameters - * - * @internal should be used only internally - */ -export function toComputed< - T, - TReturn, - TFunction extends (...args: any) => TReturn, ->( - notifier: Accessor, - fn: TFunction, - debugName: string, - factory: ReactivityFeatureFactoryOptions, -): ComputedFunction { - const { createMemo } = factory - const hasArgs = getFnArgsLength(fn) > 0 - if (!hasArgs) { - const computedFn = createMemo(() => { - void notifier() - return fn() - }) - Object.defineProperty(computedFn, 'name', { value: debugName }) - markReactive(computedFn) - return computedFn as ComputedFunction - } - - const computedFn: ((this: unknown, ...argsArray: Array) => unknown) & { - _reactiveCache?: Record> - } = function (this: unknown, ...argsArray: Array) { - const cacheable = - argsArray.length === 0 || - argsArray.every((arg) => { - return ( - arg === null || - arg === undefined || - typeof arg === 'string' || - typeof arg === 'number' || - typeof arg === 'boolean' || - typeof arg === 'symbol' - ) - }) - if (!cacheable) { - return fn.apply(this, argsArray) - } - const serializedArgs = serializeArgs(...argsArray) - if ((computedFn._reactiveCache ??= {})[serializedArgs]) { - return computedFn._reactiveCache[serializedArgs]() - } - const computedSignal = createMemo(() => { - void notifier() - return fn.apply(this, argsArray) - }) - - computedFn._reactiveCache[serializedArgs] = computedSignal - - return computedSignal() - } - - Object.defineProperty(computedFn, 'name', { value: debugName }) - markReactive(computedFn) - - return computedFn as ComputedFunction -} - -function serializeArgs(...args: Array) { - return JSON.stringify(args) -} - -function getFnArgsLength( - fn: ((...args: any) => any) & { originalArgsLength?: number }, -): number { - return Math.max(0, getMemoFnMeta(fn)?.originalArgsLength ?? fn.length) -} - -function assignReactivePrototypeAPI( - notifier: Accessor, - prototype: Record, - fnName: string, - factory: ReactivityFeatureFactoryOptions, -) { - if (isReactive(prototype[fnName])) return - - const { createMemo } = factory - - const fn = prototype[fnName] - const originalArgsLength = getFnArgsLength(fn) - - if (originalArgsLength <= 1) { - Object.defineProperty(prototype, fnName, { - enumerable: true, - configurable: true, - get(this) { - const self = this - // Create a cache in the current prototype to allow the signals - // to be garbage collected. Shorthand for a WeakMap implementation - self._reactiveCache ??= {} - const cached = (self._reactiveCache[`${self.id}${fnName}`] ??= - createMemo(() => { - notifier() - return fn.apply(self) - })) - markReactive(cached) - cached[$internalMemoFnMeta] = { - originalArgsLength, - } satisfies MemoFnMeta - return cached - }, - }) - } else { - prototype[fnName] = function (this: unknown, ...args: Array) { - notifier() - return fn.apply(this, args) - } - markReactive(prototype[fnName]) - prototype[fnName][$internalMemoFnMeta] = { - originalArgsLength, - } satisfies MemoFnMeta - } -} - -export function setReactivePropertiesOnObject( - notifier: Accessor, - obj: { [key: string]: any }, - options: { - overridePrototype?: boolean - skipProperty: (property: string) => boolean - factory: ReactivityFeatureFactoryOptions - }, -) { - const { skipProperty, factory } = options - if (isReactive(obj)) { - return - } - markReactive(obj) - - for (const property in obj) { - const value = obj[property] - if (typeof value !== 'function' || skipProperty(property)) { - continue - } - defineLazyComputedProperty(notifier, { - valueFn: value, - property, - originalObject: obj, - overridePrototype: options.overridePrototype, - factory: factory, - }) - } -} diff --git a/packages/table-core/src/index.ts b/packages/table-core/src/index.ts index 099953ad94..656947d3c4 100755 --- a/packages/table-core/src/index.ts +++ b/packages/table-core/src/index.ts @@ -80,7 +80,6 @@ export * from './features/stockFeatures' // tableReactivityFeature export * from './features/table-reactivity/tableReactivityFeature' -export * from './features/table-reactivity/tableReactivityFeature.utils' // columnFacetingFeature export * from './features/column-faceting/columnFacetingFeature' diff --git a/packages/table-core/src/utils.ts b/packages/table-core/src/utils.ts index d8325b6f72..18877a09f3 100755 --- a/packages/table-core/src/utils.ts +++ b/packages/table-core/src/utils.ts @@ -16,7 +16,7 @@ export function makeStateUpdater< K extends (string & {}) | keyof TableState_All | keyof TableState, >(key: K, instance: Table) { return (updater: Updater[K & keyof TableState]>) => { - ;(instance as Table_Internal).baseStore.setState( + instance.baseStore.setState( (old: TTableState) => { return { ...old, diff --git a/packages/table-devtools/package.json b/packages/table-devtools/package.json index 764f5c2e59..083b7ead37 100644 --- a/packages/table-devtools/package.json +++ b/packages/table-devtools/package.json @@ -59,7 +59,6 @@ "@tanstack/devtools-ui": "^0.4.4", "@tanstack/devtools-utils": "^0.3.0", "@tanstack/solid-store": "^0.9.1", - "clsx": "^2.1.1", "goober": "^2.1.18", "solid-js": "^1.9.11" }, diff --git a/packages/table-devtools/src/TableContextProvider.tsx b/packages/table-devtools/src/TableContextProvider.tsx index e2d8ad5f21..8d043e2b9f 100644 --- a/packages/table-devtools/src/TableContextProvider.tsx +++ b/packages/table-devtools/src/TableContextProvider.tsx @@ -10,15 +10,10 @@ import { subscribeTableDevtoolsTarget, } from './tableTarget' import type { Accessor, ParentComponent, Setter } from 'solid-js' -import type { RowData, Table, TableFeatures } from '@tanstack/table-core' +import type { RowData, Table, stockFeatures } from '@tanstack/table-core' -export type TableDevtoolsTabId = - | 'features' - | 'state' - | 'options' - | 'rows' - | 'columns' -type AnyTable = Table +type TableDevtoolsTabId = 'features' | 'state' | 'options' | 'rows' | 'columns' +type AnyTable = Table interface TableDevtoolsContextValue { table: Accessor diff --git a/packages/table-devtools/src/components/FeaturesPanel.tsx b/packages/table-devtools/src/components/FeaturesPanel.tsx index 06a5fb4f30..42954aa92a 100644 --- a/packages/table-devtools/src/components/FeaturesPanel.tsx +++ b/packages/table-devtools/src/components/FeaturesPanel.tsx @@ -188,15 +188,9 @@ export function FeaturesPanel() { {(rowModelName) => { const fns = getRowModelFunctions(rowModelName) - const count = getRowCountForModel( - tableInstance as { [key: string]: unknown }, - rowModelName, - ) return (
-
- {rowModelName} ({count.toLocaleString()} rows) -
+
{rowModelName}
{(fnName) => (
{fnName}
diff --git a/packages/table-devtools/src/components/OptionsPanel.tsx b/packages/table-devtools/src/components/OptionsPanel.tsx index 011be836e8..8bd669db21 100644 --- a/packages/table-devtools/src/components/OptionsPanel.tsx +++ b/packages/table-devtools/src/components/OptionsPanel.tsx @@ -11,7 +11,7 @@ export function OptionsPanel() { const tableInstance = table() const tableState = tableInstance ? useStore( - tableInstance.baseOptionsStore, + tableInstance.optionsStore, ({ state, data, _features, _rowModels, ...options }) => options, ) : undefined diff --git a/packages/vue-table/src/createTableHelper.ts b/packages/vue-table/src/createTableHelper.ts index fad1c0bd77..57582886ef 100644 --- a/packages/vue-table/src/createTableHelper.ts +++ b/packages/vue-table/src/createTableHelper.ts @@ -12,7 +12,7 @@ import type { export type TableHelper< TFeatures extends TableFeatures, TData extends RowData = any, -> = Omit, 'tableCreator'> & { +> = Omit, 'tableCreator'> & { useTable: ( tableOptions: Omit< TableOptionsWithReactiveData, @@ -26,9 +26,9 @@ export function createTableHelper< TFeatures extends TableFeatures, TData extends RowData, >( - tableHelperOptions: TableHelperOptions, + tableHelperOptions: TableHelperOptions, ): TableHelper { - const tableHelper = constructTableHelper(useTable, tableHelperOptions) + const tableHelper = constructTableHelper(useTable as any, tableHelperOptions) return { ...tableHelper, useTable: ( diff --git a/packages/vue-table/src/useTable.ts b/packages/vue-table/src/useTable.ts index 27d6dc7fc2..8d6ef2bfdf 100644 --- a/packages/vue-table/src/useTable.ts +++ b/packages/vue-table/src/useTable.ts @@ -1,5 +1,8 @@ -import { isRef, unref, watch } from 'vue' -import { constructTable } from '@tanstack/table-core' +import { isRef, ref, unref, watch, watchEffect } from 'vue' +import { + constructReactivityFeature, + constructTable, +} from '@tanstack/table-core' import { useStore } from '@tanstack/vue-store' import { mergeProxy } from './merge-proxy' import type { @@ -74,76 +77,76 @@ export function useTable< selector: (state: TableState) => TSelected = () => ({}) as TSelected, ): VueTable { + const notifier = ref(0) + + const vueReactivityFeature = constructReactivityFeature({ + stateNotifier: () => notifier.value, + optionsNotifier: () => notifier.value, + }) + const IS_REACTIVE = isRef(tableOptions.data) - const statefulOptions = mergeProxy( - IS_REACTIVE ? getOptionsWithReactiveData(tableOptions) : tableOptions, + const mergedOptions = { + ...tableOptions, + _features: { + ...tableOptions._features, + vueReactivityFeature, + }, + } + + const resolvedOptions = mergeProxy( + IS_REACTIVE + ? getOptionsWithReactiveData( + mergedOptions as TableOptions, + ) + : mergedOptions, { // Remove state and onStateChange - store handles it internally mergeOptions: ( defaultOptions: TableOptions, newOptions: Partial>, ) => { - return IS_REACTIVE - ? { - ...defaultOptions, - ...newOptions, - } - : mergeProxy(defaultOptions, newOptions) + return mergeProxy(defaultOptions, newOptions) }, }, ) as TableOptions - const table = constructTable(statefulOptions) as VueTable< + const table = constructTable(resolvedOptions) as VueTable< TFeatures, TData, TSelected > - function updateOptions() { - table.setOptions((prev) => { - return mergeProxy( - prev, - IS_REACTIVE ? getOptionsWithReactiveData(tableOptions) : tableOptions, - ) as TableOptions - }) - } - - updateOptions() - - // Add reactivity support for reactive data - if (IS_REACTIVE) { - watch( - () => tableOptions.data, - () => { - table.baseStore.setState((prev: TableState) => ({ - ...prev, - data: unref(tableOptions.data), - })) - }, - { immediate: true }, - ) - } - - /** - * Temp force reactivity to all state changes on every table.get* method - */ const allState = useStore(table.store, (state) => state) + const allOptions = useStore(table.optionsStore, (state) => state) - // Wrap all "get*" methods to make them reactive - // Access allState.value directly to create reactive dependency - Object.keys(table).forEach((key) => { - const value = (table as any)[key] - if (typeof value === 'function' && key.startsWith('get')) { - const originalMethod = value.bind(table) - ;(table as any)[key] = (...args: Array) => { - // Access state to create reactive dependency - allState.value - return originalMethod(...args) - } - } + watchEffect(() => { + allState.value + allOptions.value + notifier.value++ }) + watch( + () => + [ + IS_REACTIVE ? unref(tableOptions.data) : tableOptions.data, + tableOptions, + ] as const, + () => { + table.setOptions((prev) => { + return mergeProxy( + prev, + IS_REACTIVE + ? getOptionsWithReactiveData( + tableOptions as TableOptions, + ) + : tableOptions, + ) as TableOptions + }) + }, + { immediate: true }, + ) + table.Subscribe = function Subscribe(props: { selector: (state: TableState) => TSelected children: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5499862de..2dd23b92e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2785,7 +2785,7 @@ importers: specifier: ^9.0.0-alpha.10 version: link:../../../packages/solid-table '@tanstack/solid-table-devtools': - specifier: 9.0.0-alpha.11 + specifier: workspace:* version: link:../../../packages/solid-table-devtools solid-js: specifier: ^1.9.11 @@ -3621,10 +3621,10 @@ importers: '@tanstack/table-devtools': specifier: workspace:* version: link:../table-devtools - devDependencies: solid-js: specifier: ^1.9.11 version: 1.9.11 + devDependencies: vite-plugin-solid: specifier: ^2.11.10 version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.19.2)(yaml@2.8.2)) @@ -3674,9 +3674,6 @@ importers: '@tanstack/table-core': specifier: workspace:* version: link:../table-core - clsx: - specifier: ^2.1.1 - version: 2.1.1 goober: specifier: ^2.1.18 version: 2.1.18(csstype@3.2.3) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5add109e67..773f661ccf 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,7 +1,8 @@ +packages: + - examples/**/* + - packages/* + cleanupUnusedCatalogs: true + linkWorkspacePackages: true preferWorkspacePackages: true - -packages: - - 'examples/**/*' - - 'packages/*'