This commit is contained in:
seb
2026-10-05 22:29:22 +02:00
commit 705bd6d199
26 changed files with 6095 additions and 0 deletions

232
README.md Normal file
View File

@@ -0,0 +1,232 @@
# ⚡ react-virtual-aggridautomat
> Opinionated, fully-virtualized AG Grid component powered by [react-automat](https://github.com/sebseb7/react-automat) — infinite-scroll data grids as React `PureComponent` with zero hooks, HOCs, or Context Providers.
[![AG Grid](https://img.shields.io/badge/AG_Grid-v36-0084c7?style=flat-square)](https://www.ag-grid.com/)
[![react-automat](https://img.shields.io/badge/react--automat-v0.1-crimson?style=flat-square)](https://github.com/sebseb7/react-automat)
[![React 19](https://img.shields.io/badge/React-19-61dafb?style=flat-square)](https://react.dev/)
---
## What is this?
A reusable `<AutomatGrid />` PureComponent that wires AG Grid's [Infinite Row Model](https://www.ag-grid.com/react-data-grid/infinite-scrolling/) to a `react-automat` store. Your async data fetcher is the only thing you provide — the library handles:
- **Virtual row loading** — only visible rows exist in the DOM
- **Block caching** — configurable cache size, debounced scroll loading
- **Server-side sort & filter** — sort/filter changes flow through the automat and trigger datasource resets
- **Reactive state** — loading indicators, error state, row counts, and revision tracking all live in the automat
- **Zero hooks** — designed for `PureComponent` with synchronous constructor reads and fine-grained automat subscriptions
## Install
```bash
npm install ag-grid-community ag-grid-react react-automat
```
> This library is not yet published to npm. Clone or copy `src/lib/` into your project.
## Quick Start
### 1. Create a grid automat
```ts
// myGridAutomat.ts
import { createGridAutomat } from './lib';
export const myGridAutomat = createGridAutomat({
fetchRows: async ({ startRow, endRow, sortModel, filterModel }) => {
const res = await fetch(`/api/items?start=${startRow}&end=${endRow}`);
const { items, total } = await res.json();
return { rows: items, lastRow: total };
},
name: 'myGrid', // Visible in window.__AUTOMATS__
blockSize: 100, // Rows per block (passed to AG Grid's cacheBlockSize)
});
```
### 2. Render the grid
```tsx
// MyView.tsx
import { PureComponent } from 'react';
import { AutomatGrid } from './lib';
import { myGridAutomat } from './myGridAutomat';
class MyView extends PureComponent {
render() {
return (
<AutomatGrid
automat={myGridAutomat}
columnDefs={[
{ field: 'id', maxWidth: 80 },
{ field: 'name', flex: 2, filter: 'agTextColumnFilter' },
{ field: 'price', valueFormatter: p => `€${p.value?.toFixed(2)}` },
]}
rowIdField="id"
height="600px"
/>
);
}
}
```
### 3. React to grid state
The automat is a standard `react-automat` store — subscribe to it from any PureComponent:
```tsx
class StatusBar extends PureComponent {
constructor(props) {
super(props);
this.state = {
loading: myGridAutomat.state.loading,
totalRows: myGridAutomat.state.totalRows,
};
}
componentDidMount() {
this.unsub = myGridAutomat.subscribe(this, (s) => ({
loading: s.loading,
totalRows: s.totalRows,
}));
}
componentWillUnmount() {
this.unsub();
}
render() {
return (
<div>
{this.state.loading ? 'Loading…' : `${this.state.totalRows} rows`}
<button onClick={() => myGridAutomat.actions.reload()}>↻ Reload</button>
</div>
);
}
}
```
## API Reference
### `createGridAutomat(options)`
Creates a `react-automat` store to back the grid.
| Option | Type | Default | Description |
|---|---|---|---|
| `fetchRows` | `(params: GridFetchParams) => Promise<GridFetchResult>` | **required** | Async function called by AG Grid's datasource for each block |
| `name` | `string` | `null` | Automat registry name (visible in `window.__AUTOMATS__`) |
| `blockSize` | `number` | `100` | Rows per cache block |
#### `GridFetchParams`
```ts
{
startRow: number;
endRow: number;
sortModel: SortModelItem[];
filterModel: Record<string, unknown>;
}
```
#### `GridFetchResult<TRow>`
```ts
{
rows: TRow[];
lastRow?: number; // Total count. undefined/-1 = unknown (infinite scroll continues)
}
```
#### Automat State
| Field | Type | Description |
|---|---|---|
| `ready` | `boolean` | `true` after first block loads |
| `loading` | `boolean` | `true` while any block is in-flight |
| `totalRows` | `number` | Known row count (`-1` if unknown) |
| `sortModel` | `SortModelItem[]` | Current sort state |
| `filterModel` | `Record<string, unknown>` | Current filter state |
| `revision` | `number` | Increments on reload/sort/filter change |
| `error` | `string \| null` | Last fetch error message |
#### Automat Actions
| Action | Description |
|---|---|
| `reload()` | Bumps revision, clears cache, forces full refetch |
| `setSort(sortModel)` | Updates sort model + reloads |
| `setFilter(filterModel)` | Updates filter model + reloads |
### `<AutomatGrid />` Props
| Prop | Type | Default | Description |
|---|---|---|---|
| `automat` | `Automat<GridAutomatState, GridAutomatActions>` | **required** | The grid automat store |
| `columnDefs` | `ColDef[]` | **required** | AG Grid column definitions |
| `defaultColDef` | `ColDef` | `{ flex: 1, minWidth: 100, sortable: true, resizable: true }` | Default column config |
| `rowIdField` | `string` | `'id'` | Row data field used for stable row IDs |
| `cacheBlockSize` | `number` | automat's `blockSize` | Override cache block size |
| `rowBuffer` | `number` | `0` | Extra rows rendered outside viewport |
| `infiniteInitialRowCount` | `number` | `200` | Scrollbar size hint |
| `maxConcurrentDatasourceRequests` | `number` | `2` | Parallel block fetches |
| `maxBlocksInCache` | `number` | `10` | Max cached blocks before eviction |
| `blockLoadDebounceMillis` | `number` | `100` | Scroll debounce |
| `height` | `string \| number` | `'100%'` | Container height |
| `width` | `string \| number` | `'100%'` | Container width |
| `rowHeight` | `number` | AG Grid default (42) | Row height in pixels |
| `theme` | `string` | `'ag-theme-alpine ag-theme-alpine-dark'` | Container theme class |
| `modules` | `Module[]` | InfiniteRowModel + Text/Number/Date/Custom filters | Additional AG Grid modules |
| `onGridApiReady` | `(api: GridApi) => void` | — | Callback when AG Grid API is available |
| `className` | `string` | — | Container CSS class |
| `style` | `CSSProperties` | — | Container inline styles |
## Data Flow
```
┌─────────────────────────────────────────────────────────┐
│ AutomatGrid (PureComponent) │
│ │
│ constructor: reads automat.state synchronously │
│ componentDidMount: subscribes with selector │
│ componentDidUpdate: if revision changed → reset ds │
│ │
│ ┌──────────────┐ ┌────────────────────────┐ │
│ │ AG Grid │────▶│ IDatasource.getRows() │ │
│ │ (infinite │ │ calls automat._fetcher │ │
│ │ row model) │◀────│ → successCallback() │ │
│ └──────────────┘ └────────────────────────┘ │
│ │ │ │
│ onSortChanged ──────▶ automat.actions.setSort() │
│ onFilterChanged ────▶ automat.actions.setFilter() │
│ │ │ │
│ └──── revision bump ─────┘ │
└─────────────────────────────────────────────────────────┘
```
## Running the Demo
```bash
npm install
npm run dev
```
The demo generates 10,000 fake product rows and serves them through a simulated async fetcher with artificial latency. Open http://localhost:5173 to see infinite scroll in action.
## Project Structure
```
src/
├── lib/ # Reusable library
│ ├── createGridAutomat.ts # Automat factory
│ ├── AutomatGrid.tsx # PureComponent grid wrapper
│ └── index.ts # Barrel exports
└── demo/ # Demo app
├── demoAutomat.ts # 10k fake rows + fetcher
└── DemoGrid.tsx # Demo consumer component
```
## License
0BSD