Skip to content

Commit 7ebdbe7

Browse files
author
dasathyakuma
committed
get some more things fixed
1 parent a3ee4b2 commit 7ebdbe7

File tree

6 files changed

+171
-100
lines changed

6 files changed

+171
-100
lines changed

.github/workflows/storybook-pages.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ jobs:
1313
with:
1414
node-version: 20
1515
- run: npm install
16-
- run: npm run build # build the adapter first
16+
- run: npm run build # build the adapter first
1717
- run: npm install
1818
working-directory: demo
1919
- run: npm run build-storybook
2020
working-directory: demo
2121
- uses: peaceiris/actions-gh-pages@v4
2222
with:
2323
github_token: ${{ secrets.GITHUB_TOKEN }}
24-
publish_dir: demo/storybook-static
24+
publish_dir: demo/storybook-static

README.md

Lines changed: 110 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# tanstack-table-markojs
1+
# marko-table
22

33
> Marko 6 adapter for [@tanstack/table-core](https://tanstack.com/table) — SSR, CSR, and virtualized tables with full resumability support.
44
@@ -28,10 +28,9 @@ Marko's `_const` runtime function uses strict reference equality (`!==`) to dete
2828
// Marko runtime (simplified)
2929
function _const(key, fn) {
3030
return (scope, value) => {
31-
if (scope[key] !== value) {
32-
// ← strict equality
31+
if (scope[key] !== value) { // ← strict equality
3332
scope[key] = value;
34-
fn(scope); // only propagates if value changed
33+
fn(scope); // only propagates if value changed
3534
}
3635
};
3736
}
@@ -88,10 +87,12 @@ static const columns = [
8887
8988
// ── Serializable state in <let> ───────────────────────────────────────────────
9089
// These are written to the resume frame. Every value must be JSON-serializable.
90+
// Use `as Type` casts on empty arrays/objects so the Marko language server
91+
// infers the correct type rather than `never[]` or `{}`.
9192
<let/tableId = generateTableId() />
92-
<let/sorting: SortingState = [] />
93-
<let/pagination: PaginationState = { pageIndex: 0, pageSize: 10 } />
94-
<let/rowSelection: RowSelectionState = {} />
93+
<let/sorting = ([] as SortingState) />
94+
<let/pagination = ({ pageIndex: 0, pageSize: 10 } as PaginationState) />
95+
<let/rowSelection = ({} as RowSelectionState) />
9596
<let/globalFilter = "" />
9697
9798
// ── IIFE: all table reads are local to this function ─────────────────────────
@@ -113,7 +114,8 @@ static const columns = [
113114
{ sorting, pagination, rowSelection, globalFilter },
114115
(updater) => {
115116
const cur = { sorting, pagination, rowSelection, globalFilter };
116-
const nxt = typeof updater === "function" ? updater(cur) : updater;
117+
// Cast nxt as typeof cur so property accesses are typed correctly
118+
const nxt = (typeof updater === "function" ? updater(cur) : updater) as typeof cur;
117119
if (nxt.sorting !== sorting) sorting = nxt.sorting;
118120
if (nxt.pagination !== pagination) pagination = nxt.pagination;
119121
if (nxt.rowSelection !== rowSelection) rowSelection = nxt.rowSelection;
@@ -122,11 +124,10 @@ static const columns = [
122124
);
123125
124126
return {
125-
// Pre-map TanStack objects to plain serializable values
126127
tableRows: t.getRowModel().rows.map(row => ({
127-
id: row.id, // string ✓
128-
isSelected: row.getIsSelected(), // boolean ✓
129-
original: { ...row.original }, // plain data ✓
128+
id: row.id,
129+
isSelected: row.getIsSelected(),
130+
original: { ...row.original },
130131
cells: row.getVisibleCells().map(cell => ({
131132
id: cell.id,
132133
colId: cell.column.id,
@@ -152,7 +153,7 @@ static const columns = [
152153
};
153154
})() />
154155
155-
<effect() { return () => destroyTable(tableId) } />
156+
<script() { return () => destroyTable(tableId) } />
156157
157158
<table>
158159
<thead>
@@ -208,11 +209,11 @@ import { syncMarkoTable, generateTableId, destroyTable, flexRender,
208209
209210
<let/mounted = false />
210211
<let/tableId = generateTableId() />
211-
<let/sorting: SortingState = [] />
212-
<let/pagination: PaginationState = { pageIndex: 0, pageSize: 10 } />
212+
<let/sorting = ([] as SortingState) />
213+
<let/pagination = ({ pageIndex: 0, pageSize: 10 } as PaginationState) />
213214
214-
<effect() { mounted = true } />
215-
<effect() { return () => destroyTable(tableId) } />
215+
<script() { mounted = true } />
216+
<script() { return () => destroyTable(tableId) } />
216217
217218
<if=!mounted>
218219
<div>Loading...</div>
@@ -221,15 +222,18 @@ import { syncMarkoTable, generateTableId, destroyTable, flexRender,
221222
<if=mounted>
222223
// Same IIFE pattern — same-reference issue applies regardless of SSR
223224
<const/view = (() => {
224-
const t = syncMarkoTable(tableId, { ... }, { sorting, pagination }, setState);
225+
const t = syncMarkoTable(tableId, { ... }, { sorting, pagination }, (updater) => {
226+
const cur = { sorting, pagination };
227+
const nxt = (typeof updater === "function" ? updater(cur) : updater) as typeof cur;
228+
if (nxt.sorting !== sorting) sorting = nxt.sorting;
229+
if (nxt.pagination !== pagination) pagination = nxt.pagination;
230+
});
225231
return {
226232
tableRows: t.getRowModel().rows.map(row => ({ ... })),
227233
// ... etc
228234
};
229235
})() />
230236
231-
// Inside <if=mounted>, Marko doesn't serialize content.
232-
// So you CAN close over `view` in handlers (it's a plain object).
233237
<table>
234238
<for|row| of=view.tableRows>
235239
<tr>...</tr>
@@ -252,15 +256,15 @@ import {
252256
253257
<let/mounted = false />
254258
<let/tableId = generateTableId() />
255-
<let/sorting: SortingState = [] />
259+
<let/sorting = ([] as SortingState) />
256260
<let/globalFilter = "" />
257261
// VirtualRow elements are plain objects — serializable
258-
<let/virtualRows: VirtualRow[] = [] />
262+
<let/virtualRows = ([] as VirtualRow[]) />
259263
<let/paddingTop = 0 />
260264
<let/paddingBottom = 0 />
261265
262-
<effect() { mounted = true } />
263-
<effect() { return () => destroyTable(tableId) } />
266+
<script() { mounted = true } />
267+
<script() { return () => destroyTable(tableId) } />
264268
265269
<if=mounted>
266270
<const/view = (() => {
@@ -269,7 +273,12 @@ import {
269273
{ data: input.data, columns, getCoreRowModel: getCoreRowModel(),
270274
getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel() },
271275
{ sorting, globalFilter },
272-
setState,
276+
(updater) => {
277+
const cur = { sorting, globalFilter };
278+
const nxt = (typeof updater === "function" ? updater(cur) : updater) as typeof cur;
279+
if (nxt.sorting !== sorting) sorting = nxt.sorting;
280+
if (nxt.globalFilter !== globalFilter) globalFilter = nxt.globalFilter;
281+
},
273282
);
274283
275284
const rows = t.getRowModel().rows;
@@ -294,7 +303,7 @@ import {
294303
295304
// Effect re-runs when view.rowCount changes (after filter/sort)
296305
// measure() forces virtualizer to recalculate and fires onChange synchronously
297-
<effect() {
306+
<script() {
298307
syncVirtualizer(tableId, `scroll-${tableId}`, view.rowCount, () => 49,
299308
(vRows, top, bot) => { virtualRows = vRows; paddingTop = top; paddingBottom = bot; });
300309
} />
@@ -329,12 +338,12 @@ import {
329338

330339
Creates or retrieves a TanStack Table instance and syncs options with current reactive state.
331340

332-
| Parameter | Type | Description |
333-
| -------------- | ------------------------- | --------------------------------------------------- |
334-
| `tableId` | `string` | ID from `generateTableId()`, stored in `<let>` |
335-
| `options` | `TableOptions<TData>` | Any valid TanStack Table options |
336-
| `currentState` | `Record<string, unknown>` | Current values of all state `<let>` signals |
337-
| `setState` | `(updater) => void` | Writes TanStack state changes back to Marko signals |
341+
| Parameter | Type | Description |
342+
|---|---|---|
343+
| `tableId` | `string` | ID from `generateTableId()`, stored in `<let>` |
344+
| `options` | `TableOptions<TData>` | Any valid TanStack Table options |
345+
| `currentState` | `Record<string, unknown>` | Current values of all state `<let>` signals |
346+
| `setState` | `(updater) => void` | Writes TanStack state changes back to Marko signals |
338347

339348
Returns: `Table<TData>` — the live table instance (same reference every call).
340349

@@ -344,7 +353,7 @@ Returns: `Table<TData>` — the live table instance (same reference every call).
344353

345354
### `generateTableId()`
346355

347-
Returns a unique string ID. Store in `<let/tableId>` — it's serializable and survives SSR→client.
356+
Returns a unique string ID. Store in `<let/tableId = generateTableId() />` — it's serializable and survives SSR→client.
348357

349358
---
350359

@@ -353,18 +362,17 @@ Returns a unique string ID. Store in `<let/tableId>` — it's serializable and s
353362
Retrieves a table instance from the module cache by ID. Use in event handlers that can't close over `t` (SSR components).
354363

355364
```marko
356-
// In SSR handlers, never close over `t` — use getTable instead
357365
onClick=() => getTable(tableId)?.firstPage()
358366
```
359367

360368
---
361369

362370
### `destroyTable(id)`
363371

364-
Removes the table instance from the cache. Call in `<effect>` cleanup.
372+
Removes the table instance from the cache. Call in `<script>` cleanup.
365373

366374
```marko
367-
<effect() { return () => destroyTable(tableId) } />
375+
<script() { return () => destroyTable(tableId) } />
368376
```
369377

370378
---
@@ -383,21 +391,21 @@ ${ flexRender(cell.column.columnDef.cell, cell.getContext()) }
383391

384392
Creates or updates a row virtualizer. Requires `@tanstack/virtual-core` v3+.
385393

386-
| Parameter | Type | Description |
387-
| -------------- | ------------------------------------------- | -------------------------------------- |
388-
| `tableId` | `string` | Same ID as `syncMarkoTable` |
389-
| `scrollElId` | `string` | `id` attribute of the scroll container |
390-
| `count` | `number` | Total filtered row count |
391-
| `estimateSize` | `(i: number) => number` | Estimated row height in pixels |
392-
| `onUpdate` | `(rows, paddingTop, paddingBottom) => void` | Called when virtual items change |
394+
| Parameter | Type | Description |
395+
|---|---|---|
396+
| `tableId` | `string` | Same ID as `syncMarkoTable` |
397+
| `scrollElId` | `string` | `id` attribute of the scroll container |
398+
| `count` | `number` | Total filtered row count |
399+
| `estimateSize` | `(i: number) => number` | Estimated row height in pixels |
400+
| `onUpdate` | `(rows, paddingTop, paddingBottom) => void` | Called when virtual items change |
393401

394-
Call inside `<effect>` so the scroll container exists in the DOM.
402+
Call inside `<script>` so the scroll container exists in the DOM.
395403

396404
---
397405

398-
### `preloadVirtualizer()`
406+
### `destroyVirtualizer(id)`
399407

400-
Pre-loads `@tanstack/virtual-core` in ESM-only environments where `require()` is unavailable.
408+
Removes a virtualizer instance from the cache. Called automatically by `destroyTable`.
401409

402410
---
403411

@@ -417,7 +425,33 @@ Pre-loads `@tanstack/virtual-core` in ESM-only environments where `require()` is
417425
})() />
418426
```
419427

420-
### 2. Use `checkedChange` not `onChange` for controlled checkboxes
428+
### 2. Use `as` casts on typed `<let>` initial values
429+
430+
The Marko language server infers `never[]` from `[]` and `{}` without a type hint. Use `as` casts so types resolve correctly:
431+
432+
```marko
433+
// ❌ Language server infers never[] — downstream type errors
434+
<let/sorting: SortingState = [] />
435+
436+
// ✅ Type inferred correctly from the cast
437+
<let/sorting = ([] as SortingState) />
438+
<let/rowSelection = ({} as RowSelectionState) />
439+
```
440+
441+
### 3. Cast `nxt as typeof cur` in the setState callback
442+
443+
`syncMarkoTable`'s `setState` is typed as `Updater<Record<string, unknown>>`, so `updater(cur)` returns `Record<string, unknown>` — every property access is `unknown` without the cast:
444+
445+
```marko
446+
(updater) => {
447+
const cur = { sorting, pagination };
448+
// ✅ Cast nxt so nxt.sorting has type SortingState, not unknown
449+
const nxt = (typeof updater === "function" ? updater(cur) : updater) as typeof cur;
450+
if (nxt.sorting !== sorting) sorting = nxt.sorting;
451+
}
452+
```
453+
454+
### 4. Use `checkedChange` not `onChange` for controlled checkboxes
421455

422456
```marko
423457
// ❌ Preserves old visual state (Marko's uncontrolled mode)
@@ -427,7 +461,7 @@ Pre-loads `@tanstack/virtual-core` in ESM-only environments where `require()` is
427461
<input type="checkbox" checked=row.isSelected checkedChange=(v) => {...} />
428462
```
429463

430-
### 3. Pre-map all TanStack objects to plain values before the template
464+
### 5. Pre-map all TanStack objects to plain values before the template
431465

432466
TanStack `Row`, `Cell`, `Header`, and `Column` objects contain functions and cannot be serialized. Extract all needed values inside the IIFE:
433467

@@ -447,7 +481,21 @@ TanStack `Row`, `Cell`, `Header`, and `Column` objects contain functions and can
447481
})() />
448482
```
449483

450-
### 4. Avoid `>` in `<const>` expressions
484+
### 6. Use `<script>` not `<effect>`
485+
486+
`<effect>` is deprecated. Use `<script>` for all side effects and cleanup:
487+
488+
```marko
489+
// ❌ Deprecated
490+
<effect() { mounted = true } />
491+
<effect() { return () => destroyTable(tableId) } />
492+
493+
// ✅ Current
494+
<script() { mounted = true } />
495+
<script() { return () => destroyTable(tableId) } />
496+
```
497+
498+
### 7. Avoid `>` in `<const>` expressions
451499

452500
Marko's HTML parser treats `>` as a tag-close character:
453501

@@ -457,10 +505,9 @@ Marko's HTML parser treats `>` as a tag-close character:
457505
458506
// ✅ Use truthy check or !== instead
459507
<const/hasFilters = !!filters.length />
460-
<const/hasFilters = filters.length !== 0 />
461508
```
462509

463-
### 5. Event handlers must only close over serializable values
510+
### 8. Event handlers must only close over serializable values
464511

465512
In SSR components, anything captured in a handler closure is serialized. Only close over `string`, `number`, `boolean`, or `<let>` signals:
466513

@@ -480,17 +527,17 @@ onClick=() => {
480527

481528
## SSR + Resume: what actually gets serialized
482529

483-
| In resume frame | Not in resume frame |
484-
| --------------------------------------------- | --------------------------------------------- |
485-
| `tableId` (string) | Table instance (functions, class prototype) |
486-
| `sorting` (array of plain objects) | Row objects |
487-
| `pagination` (plain object) | Header/Cell/Column objects |
488-
| `rowSelection` (plain string/boolean map) | `flexRender` output (recomputed from signals) |
489-
| `globalFilter` (string) | |
490-
| `columnFilters` (array of plain objects) | |
491-
| `columnVisibility` (plain string/boolean map) | |
492-
| `columnSizing` (plain string/number map) | |
493-
| `expanded` (plain string/boolean map) | |
530+
| In resume frame | Not in resume frame |
531+
|---|---|
532+
| `tableId` (string) | Table instance (functions, class prototype) |
533+
| `sorting` (array of plain objects) | Row objects |
534+
| `pagination` (plain object) | Header/Cell/Column objects |
535+
| `rowSelection` (plain string/boolean map) | `flexRender` output (recomputed from signals) |
536+
| `globalFilter` (string) | |
537+
| `columnFilters` (array of plain objects) | |
538+
| `columnVisibility` (plain string/boolean map) | |
539+
| `columnSizing` (plain string/number map) | |
540+
| `expanded` (plain string/boolean map) | |
494541

495542
On the server: table rows are fully rendered to HTML. On the client: Marko restores the signals from the resume frame, the first interaction triggers the IIFE, `syncMarkoTable` recreates the table instance with the correct state, and the reactive cycle proceeds.
496543

@@ -500,4 +547,4 @@ On the server: table rows are fully rendered to HTML. On the client: Marko resto
500547

501548
### 0.1.0
502549

503-
Initial release.
550+
Initial release.

0 commit comments

Comments
 (0)