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-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()], +}) 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/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..e2d8ad5f21 100644 --- a/packages/table-devtools/src/TableContextProvider.tsx +++ b/packages/table-devtools/src/TableContextProvider.tsx @@ -9,11 +9,15 @@ import { getTableDevtoolsTarget, subscribeTableDevtoolsTarget, } from './tableTarget' - import type { Accessor, ParentComponent, Setter } from 'solid-js' import type { RowData, Table, TableFeatures } from '@tanstack/table-core' -type TableDevtoolsTabId = 'features' | 'state' | 'rows' | 'columns' +export type TableDevtoolsTabId = + | 'features' + | 'state' + | 'options' + | 'rows' + | 'columns' type AnyTable = Table interface TableDevtoolsContextValue { 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 5a454a7d8c..768b85dbc1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2775,9 +2775,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 @@ -3588,11 +3594,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 @@ -3640,9 +3665,15 @@ 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 + clsx: + specifier: ^2.1.1 + version: 2.1.1 goober: specifier: ^2.1.18 version: 2.1.18(csstype@3.2.3) @@ -7213,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 @@ -15717,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': {}