@octanejs/tanstack-table 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/package.json +51 -0
- package/src/index.ts +98 -0
- package/src/internal.ts +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominic Gannaway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @octanejs/tanstack-table
|
|
2
|
+
|
|
3
|
+
[TanStack Table](https://tanstack.com/table) for the [octane](https://github.com/octanejs/octane) UI framework.
|
|
4
|
+
|
|
5
|
+
TanStack Table separates a framework-agnostic core (`@tanstack/table-core`:
|
|
6
|
+
`createTable` plus every feature row model — sorting, filtering, pagination,
|
|
7
|
+
selection, visibility, expanding, grouping, faceting, …) from a ~100-line React
|
|
8
|
+
adapter (`useReactTable` + `flexRender`). This package reuses the core
|
|
9
|
+
unchanged (re-exported verbatim) and transcribes only the adapter onto octane's
|
|
10
|
+
hooks, preserving upstream's exact `useState`-based state wiring. The public
|
|
11
|
+
surface matches `@tanstack/react-table` 1:1 — existing code works by changing
|
|
12
|
+
the import.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
// before
|
|
16
|
+
import { useReactTable, flexRender, getCoreRowModel } from '@tanstack/react-table';
|
|
17
|
+
// after
|
|
18
|
+
import { useReactTable, flexRender, getCoreRowModel } from '@octanejs/tanstack-table';
|
|
19
|
+
|
|
20
|
+
function People() @{
|
|
21
|
+
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() });
|
|
22
|
+
<table>
|
|
23
|
+
<thead>
|
|
24
|
+
@for (const hg of table.getHeaderGroups(); key hg.id) {
|
|
25
|
+
<tr>
|
|
26
|
+
@for (const header of hg.headers; key header.id) {
|
|
27
|
+
<th onClick={header.column.getToggleSortingHandler()}>
|
|
28
|
+
{header.isPlaceholder
|
|
29
|
+
? null
|
|
30
|
+
: flexRender(header.column.columnDef.header, header.getContext())}
|
|
31
|
+
</th>
|
|
32
|
+
}
|
|
33
|
+
</tr>
|
|
34
|
+
}
|
|
35
|
+
</thead>
|
|
36
|
+
<tbody>
|
|
37
|
+
@for (const row of table.getRowModel().rows; key row.id) {
|
|
38
|
+
<tr>
|
|
39
|
+
@for (const cell of row.getVisibleCells(); key cell.id) {
|
|
40
|
+
<td>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
|
|
41
|
+
}
|
|
42
|
+
</tr>
|
|
43
|
+
}
|
|
44
|
+
</tbody>
|
|
45
|
+
</table>
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Entry points
|
|
50
|
+
|
|
51
|
+
| import | what you get | notes |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `@octanejs/tanstack-table` | everything `@tanstack/table-core` exports + `useReactTable`, `flexRender`, `Renderable` | core verbatim + the octane-bound adapter (single entry, mirroring upstream) |
|
|
54
|
+
|
|
55
|
+
## How it works
|
|
56
|
+
|
|
57
|
+
`useReactTable` is a line-for-line transcription of the upstream adapter: the
|
|
58
|
+
table instance is created once, its state lives in a `useState` whose setter is
|
|
59
|
+
wired into `onStateChange`, and options are re-composed into the instance
|
|
60
|
+
during every render — so partially-controlled state (`state.sorting` +
|
|
61
|
+
`onSortingChange`), full `onStateChange` passthrough, and table-core's
|
|
62
|
+
functional `Updater<T>` contract behave exactly as on React.
|
|
63
|
+
|
|
64
|
+
`flexRender` triages a columnDef renderer: components render through octane's
|
|
65
|
+
`createElement` descriptor at value position; strings, numbers, and pre-created
|
|
66
|
+
elements pass through as-is. Upstream's class-component and
|
|
67
|
+
`react.memo`/`forwardRef` exotic-object branches are dropped — octane has no
|
|
68
|
+
class components or `forwardRef`, and octane's `memo()` returns a plain
|
|
69
|
+
function, so `typeof === 'function'` covers every component.
|
|
70
|
+
|
|
71
|
+
octane keys hooks by a compiler-injected per-call-site `Symbol`, appended as
|
|
72
|
+
the last argument of every `use*` call. `useReactTable` forwards that slot into
|
|
73
|
+
its composed hooks, so two tables in one component stay independent, exactly
|
|
74
|
+
like in React.
|
|
75
|
+
|
|
76
|
+
## Status
|
|
77
|
+
|
|
78
|
+
Current scope, known divergences, and verification status are tracked in the
|
|
79
|
+
generated [bindings status table](../../docs/bindings-status.md), sourced from
|
|
80
|
+
this package's [`status.json`](./status.json).
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octanejs/tanstack-table",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"octane": {
|
|
7
|
+
"hookSlots": {
|
|
8
|
+
"manual": [
|
|
9
|
+
"src"
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"description": "TanStack Table bindings for the octane renderer — reuses the framework-agnostic @tanstack/table-core and swaps the ~100-line React adapter (useReactTable, flexRender) for an octane port.",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Dominic Gannaway",
|
|
16
|
+
"email": "dg@domgan.com"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/octanejs/octane.git",
|
|
24
|
+
"directory": "packages/tanstack-table"
|
|
25
|
+
},
|
|
26
|
+
"main": "src/index.ts",
|
|
27
|
+
"module": "src/index.ts",
|
|
28
|
+
"types": "src/index.ts",
|
|
29
|
+
"files": [
|
|
30
|
+
"src",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"exports": {
|
|
34
|
+
".": "./src/index.ts"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@tanstack/table-core": "8.21.3",
|
|
38
|
+
"octane": "0.1.3"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@tanstack/react-table": "8.21.3",
|
|
42
|
+
"@tsrx/react": "^0.2.37",
|
|
43
|
+
"esbuild": "^0.28.1",
|
|
44
|
+
"react": "^19.2.0",
|
|
45
|
+
"react-dom": "^19.2.0",
|
|
46
|
+
"vitest": "^4.1.9"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"test": "vitest run"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// @octanejs/tanstack-table — TanStack Table for the octane renderer.
|
|
2
|
+
//
|
|
3
|
+
// TanStack Table separates a framework-agnostic core (`@tanstack/table-core`:
|
|
4
|
+
// createTable + every feature row model) from a ~100-line React adapter
|
|
5
|
+
// (`useReactTable` + `flexRender`). This package reuses the core UNCHANGED
|
|
6
|
+
// (re-exported verbatim) and transcribes only the adapter onto octane's hooks,
|
|
7
|
+
// preserving upstream's exact useState-based shape — the table instance is
|
|
8
|
+
// created once, state lives in a useState whose setter is wired into the
|
|
9
|
+
// instance's onStateChange, and options are re-composed into the instance
|
|
10
|
+
// during every render. The public surface matches @tanstack/react-table 1:1:
|
|
11
|
+
// existing code works by changing the import.
|
|
12
|
+
//
|
|
13
|
+
// The one octane-specific detail is hook slots: octane keys hooks by a
|
|
14
|
+
// compiler-injected per-call-site Symbol, appended as the LAST argument of
|
|
15
|
+
// every `use*` call. `useReactTable` forwards that slot into its two useState
|
|
16
|
+
// calls (deriving a stable sub-slot for each), so two tables in one component
|
|
17
|
+
// stay independent, just like in React.
|
|
18
|
+
import { createElement, useState } from 'octane';
|
|
19
|
+
import type { ComponentBody, ElementDescriptor } from 'octane';
|
|
20
|
+
import { createTable } from '@tanstack/table-core';
|
|
21
|
+
import type { RowData, TableOptions, TableOptionsResolved } from '@tanstack/table-core';
|
|
22
|
+
import { splitSlot, subSlot } from './internal';
|
|
23
|
+
|
|
24
|
+
export * from '@tanstack/table-core';
|
|
25
|
+
|
|
26
|
+
export type Renderable<TProps> =
|
|
27
|
+
| ComponentBody<TProps>
|
|
28
|
+
| ElementDescriptor<any>
|
|
29
|
+
| string
|
|
30
|
+
| number
|
|
31
|
+
| boolean
|
|
32
|
+
| null
|
|
33
|
+
| undefined;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* If rendering headers, cells, or footers with custom markup, use flexRender
|
|
37
|
+
* instead of `cell.getValue()` or `cell.renderValue()`.
|
|
38
|
+
*
|
|
39
|
+
* Port note: upstream additionally detects class components and
|
|
40
|
+
* `react.memo`/`react.forward_ref` exotic objects. Both branches are dead in
|
|
41
|
+
* octane — there are no class components or forwardRef, and octane's `memo()`
|
|
42
|
+
* returns a plain function — so a component is exactly `typeof === 'function'`.
|
|
43
|
+
* The descriptor `createElement` returns renders at value position (a `.tsrx`
|
|
44
|
+
* hole); non-component values (strings, numbers, pre-created descriptors)
|
|
45
|
+
* pass through as-is.
|
|
46
|
+
*/
|
|
47
|
+
export function flexRender<TProps extends object>(Comp: Renderable<TProps>, props: TProps) {
|
|
48
|
+
return !Comp
|
|
49
|
+
? null
|
|
50
|
+
: typeof Comp === 'function'
|
|
51
|
+
? createElement(Comp as ComponentBody<TProps>, props)
|
|
52
|
+
: Comp;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useReactTable<TData extends RowData>(
|
|
56
|
+
options: TableOptions<TData>,
|
|
57
|
+
...rest: unknown[]
|
|
58
|
+
) {
|
|
59
|
+
const [, slot] = splitSlot(rest);
|
|
60
|
+
|
|
61
|
+
// Compose in the generic options to the user options
|
|
62
|
+
const resolvedOptions: TableOptionsResolved<TData> = {
|
|
63
|
+
state: {}, // Dummy state
|
|
64
|
+
onStateChange: () => {}, // noop
|
|
65
|
+
renderFallbackValue: null,
|
|
66
|
+
...options,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Create a new table and store it in state
|
|
70
|
+
const [tableRef] = useState(
|
|
71
|
+
() => ({
|
|
72
|
+
current: createTable<TData>(resolvedOptions),
|
|
73
|
+
}),
|
|
74
|
+
subSlot(slot, 'urt:t'),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// By default, manage table state here using the table's initial state
|
|
78
|
+
const [state, setState] = useState(() => tableRef.current.initialState, subSlot(slot, 'urt:s'));
|
|
79
|
+
|
|
80
|
+
// Compose the default state above with any user state. This will allow the user
|
|
81
|
+
// to only control a subset of the state if desired.
|
|
82
|
+
tableRef.current.setOptions((prev) => ({
|
|
83
|
+
...prev,
|
|
84
|
+
...options,
|
|
85
|
+
state: {
|
|
86
|
+
...state,
|
|
87
|
+
...options.state,
|
|
88
|
+
},
|
|
89
|
+
// Similarly, we'll maintain both our internal state and any user-provided
|
|
90
|
+
// state.
|
|
91
|
+
onStateChange: (updater) => {
|
|
92
|
+
setState(updater);
|
|
93
|
+
options.onStateChange?.(updater);
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
return tableRef.current;
|
|
98
|
+
}
|
package/src/internal.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Slot mechanics shared by the binding's plain-`.ts` hooks (same helper as
|
|
2
|
+
// @octanejs/tanstack-query). The octane compiler injects a per-call-site Symbol slot into
|
|
3
|
+
// every hook call in compiled files; these binding files are NOT compiled, so a
|
|
4
|
+
// hook here receives the caller's slot as its trailing argument and derives a
|
|
5
|
+
// distinct sub-slot for each base hook it composes.
|
|
6
|
+
|
|
7
|
+
// Memoized: subSlot runs on EVERY hook call every render; the cache returns the
|
|
8
|
+
// identical Symbol.for-interned value without the concat + registry lookup.
|
|
9
|
+
const subSlotCache = new Map<symbol, Map<string, symbol>>();
|
|
10
|
+
// Tag-only symbols for the slotless-caller case (see below).
|
|
11
|
+
const bareTagCache = new Map<string, symbol>();
|
|
12
|
+
|
|
13
|
+
export function subSlot(slot: symbol | undefined, tag: string): symbol {
|
|
14
|
+
// No inherited slot (the caller was NOT compiled — e.g. a vendored wrapper
|
|
15
|
+
// hook): return a stable TAG-ONLY symbol rather than undefined. The runtime
|
|
16
|
+
// combines it with the ambient withSlot path, so sibling base hooks inside
|
|
17
|
+
// one composed hook stay DISTINCT per tag. Returning undefined here made
|
|
18
|
+
// them all resolve to the bare path — one shared slot, state collision.
|
|
19
|
+
if (slot === undefined) {
|
|
20
|
+
let bare = bareTagCache.get(tag);
|
|
21
|
+
if (bare === undefined) bareTagCache.set(tag, (bare = Symbol.for(':' + tag)));
|
|
22
|
+
return bare;
|
|
23
|
+
}
|
|
24
|
+
let byTag = subSlotCache.get(slot);
|
|
25
|
+
if (byTag === undefined) subSlotCache.set(slot, (byTag = new Map()));
|
|
26
|
+
let sym = byTag.get(tag);
|
|
27
|
+
if (sym === undefined) byTag.set(tag, (sym = Symbol.for((slot.description ?? '') + ':' + tag)));
|
|
28
|
+
return sym;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Split the compiler-injected trailing slot off a hook's runtime args, returning
|
|
32
|
+
// the user args (everything before it) and the slot.
|
|
33
|
+
export function splitSlot(args: any[]): [any[], symbol | undefined] {
|
|
34
|
+
const tail = args[args.length - 1];
|
|
35
|
+
const slot = typeof tail === 'symbol' ? (tail as symbol) : undefined;
|
|
36
|
+
return [slot !== undefined ? args.slice(0, -1) : args, slot];
|
|
37
|
+
}
|