@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 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
+ }
@@ -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
+ }