Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,19 @@ time.
**Row key** — the value that identifies a row for its whole life, independent of
where the row currently sits. Positions move; keys do not.

**Cell kind** — a string naming how one column draws and edits. A **cell
registry** maps every kind to its renderer and its editor.
**Cell type** — the string in a column's `type` naming how that column draws
and edits. The ones this package draws are prefixed `dg:`; glide's own kinds are
named as glide names them. A **cell registry** maps every type to its maker, its
renderer, and its validator; the engine builds one per grid from the built-in
types plus the descriptor's own.

**Cell options** — one column's settings for its cell type, written on the
column and carried inside every cell that column makes. Only the cell type they
belong to reads them.

**Custom column** — a column whose `type` names a cell the host registered
rather than one this package ships. It says so with `custom: true`, which is
what lets every other column's `type` and options be checked.

## Rows and where they live

Expand Down Expand Up @@ -110,3 +121,12 @@ flash.

**Appearance watcher** — the host-owned thing that decides: an OS media query,
a host page's class, a user toggle. Always outside this package.

## Column layout

**Columns state** — the user's own arrangement of the columns: their order,
the widths they dragged, and the ones they hid. Client-owned, and the only part
of the presentation the user changes directly.

**Columns adapter** — where that arrangement is read from and written to. The
engine's default writes to Web Storage; a descriptor may name its own.
64 changes: 46 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ column layout, and typed cell editors — all against a server that holds far
more rows than the browser ever will.

> **Status: 0.x.** The API will break between minor versions. It is pinned to a
> **pre-release** build of glide-data-grid (`6.0.4-alpha24`); that pin loosens
> when glide 6 ships stable.
> **pre-release** build of glide-data-grid (`6.0.4-alpha24`). This is not a
> choice: glide's latest stable release, `6.0.3`, declares a React peer of
> `^16.12.0 || 17.x || 18.x`. React 19 support first appears in the `6.0.4`
> alpha line. The pin loosens when glide ships a stable `6.0.4`.

## What it looks like

Expand Down Expand Up @@ -66,25 +68,51 @@ import "@matthewhsu1/datagrid/datagrid.css";
// 2. Point the grid at your accent, once, before the first grid renders.
configureGridTheme({ accentColor: "grass", grayColor: "slate" });

// 3. Describe one grid.
const orderGrid = createGridInstance(
{
name: "orders",
rowKey: (row) => row.id,
columns: { defs: COLUMN_DEFS, defaultOrder: DEFAULT_ORDER },
cells: orderCells,
api: { fetchRows, fetchCount, fetchRow, updateRow },
// 3. Describe the columns. Each names its cell by type, the way glide does.
const COLUMN_DEFS = {
reference: {
field: "reference",
title: "Ref",
defaultWidth: 160,
editable: false,
type: "dg:text",
},
{ startListening: listenerMiddleware.startListening },
);

// 4. Mount its reducer under the descriptor's name, then render.
total: {
field: "total",
title: "Total",
defaultWidth: 120,
editable: true,
type: "dg:number",
options: { format: "currency", currency: "USD" },
},
customer: {
field: "customer",
title: "Customer",
defaultWidth: 200,
editable: true,
type: "text",
}, // one of glide's own kinds
};

// 4. Describe one grid.
const orderGrid = createGridInstance({
name: "orders",
rowKey: (row) => row.id,
columns: { defs: COLUMN_DEFS, defaultOrder: ["reference", "total", "customer"] },
api: { fetchRows, fetchCount, fetchRow, updateRow },
});

// 5. Mount its reducer under the descriptor's name, then render.
// <DataGrid instance={orderGrid} />
```

The grid needs a Redux store, your listener middleware, and a
`QueryClientProvider` above it. It creates its own `<div id="portal">` if your
page has none — without one, no cell can be edited and nothing says so.
Nothing registers a cell: the five the engine draws are prefixed `dg:`, glide's
own kinds keep glide's own names, and the user's column layout is remembered in
Web Storage on its own.

The grid needs a Redux store and a `QueryClientProvider` above it. It creates
its own `<div id="portal">` if your page has none — without one, no cell can be
edited and nothing says so.

## Run the demo

Expand Down Expand Up @@ -114,7 +142,7 @@ changing anything in `src/features/dataGrid/`.
## Develop

```bash
npm test # 615 tests
npm test
npm run typecheck
npm run check:docs # typechecks every code block in docs/
npm run lint
Expand Down
108 changes: 82 additions & 26 deletions demo/src/grid/testGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { startAppListening } from "../listener";
import { createGridInstance } from "@matthewhsu1/datagrid";
import { localStorageColumnsAdapter } from "@matthewhsu1/datagrid";
import type { ColumnDef, GridDescriptor, GridSliceState } from "@matthewhsu1/datagrid";
import { REGIONS, type TestRow } from "./api/types";
import {
Expand All @@ -9,42 +7,104 @@ import {
fetchTestRows,
updateTestRow,
} from "./api/testRowQueries";
import { regionCell, testGridCells } from "./testGridCells";
import { RegionHeader } from "./RegionHeader";

export const GRID_NAME = "testGrid";

/**
* Every column names its cell by `type` and carries that cell's settings on
* `options`. Nothing is registered and nothing is built: `dg:` names the cells
* the engine draws, and glide's own names are available beside them.
*
* Colors come from RADIX_BADGE_SCALES in lib/grid/radixBadgePalette.ts. That
* list is the whole `RadixColor` union — "blue", "purple", and "gray" are NOT
* in it, and each scale must also be imported in theme/radixStyles.ts.
*/
const COLUMN_DEFS: Record<string, ColumnDef> = {
id: { field: "id", title: "ID", defaultWidth: 80, editable: false, type: "int" },
name: { field: "name", title: "Name", defaultWidth: 220, editable: true, type: "text" },
sector: { field: "sector", title: "Sector", defaultWidth: 130, editable: false, type: "text" },
id: {
field: "id",
title: "ID",
defaultWidth: 80,
editable: false,
type: "dg:number",
options: { format: "integer", thousandSeparator: true, min: 0 },
},
name: { field: "name", title: "Name", defaultWidth: 220, editable: true, type: "dg:text" },
sector: { field: "sector", title: "Sector", defaultWidth: 130, editable: false, type: "dg:text" },
// Read-only because it is the GROUP field. An edit that changed a row's group
// would have to move the row, and only a server-side move bumps the data
// generation — the edit path patches a row in place. `active` is the editable
// enum this page exercises instead.
region: { field: "region", title: "Region", defaultWidth: 120, editable: false, type: "region" },
region: {
field: "region",
title: "Region",
defaultWidth: 120,
editable: false,
type: "dg:enum",
options: {
choices: [
{ value: REGIONS[0].value, label: REGIONS[0].label, color: "cyan" },
{ value: REGIONS[1].value, label: REGIONS[1].label, color: "amber" },
{ value: REGIONS[2].value, label: REGIONS[2].label, color: "plum" },
],
},
},
quantity: {
field: "quantity",
title: "Quantity",
defaultWidth: 110,
editable: true,
type: "int",
type: "dg:number",
options: { format: "integer", thousandSeparator: true, min: 0 },
},
price: {
field: "price",
title: "Price",
defaultWidth: 120,
editable: true,
type: "dg:number",
options: { format: "currency", currency: "USD", decimalScale: 2, min: 0 },
},
value: {
field: "value",
title: "Value",
defaultWidth: 140,
editable: false,
type: "dg:number",
options: { format: "currency", currency: "USD", decimalScale: 2, min: 0 },
},
contact: {
field: "contact",
title: "Contact",
defaultWidth: 160,
editable: true,
type: "dg:phone",
options: { nullable: true, defaultCountry: "US" },
},
price: { field: "price", title: "Price", defaultWidth: 120, editable: true, type: "currency" },
value: { field: "value", title: "Value", defaultWidth: 140, editable: false, type: "currency" },
contact: { field: "contact", title: "Contact", defaultWidth: 160, editable: true, type: "phone" },
updatedAt: {
field: "updatedAt",
title: "Updated",
defaultWidth: 120,
editable: true,
type: "date",
type: "dg:date",
options: { nullable: true },
},
active: {
field: "active",
title: "Active",
defaultWidth: 110,
editable: true,
type: "dg:enum",
options: {
nullable: true,
choices: [
{ value: 0, label: "Inactive", color: "tomato" },
{ value: 1, label: "Active", color: "green" },
],
},
},
active: { field: "active", title: "Active", defaultWidth: 110, editable: true, type: "active" },
};

const columnsAdapter = localStorageColumnsAdapter(`${GRID_NAME}:columns`);

/**
* The development test grid: 100,000 synthetic rows, grouped by region, with
* every editable cell type the app has.
Expand All @@ -71,6 +131,8 @@ export const testGridDescriptor: GridDescriptor<TestRow, number, number> = {
"updatedAt",
"active",
],
// `persist` and `adapter` are both left out: the layout is remembered in
// Web Storage under `datagrid:testGrid:columns` with no wiring at all.
},
grouping: {
field: "region",
Expand All @@ -80,9 +142,10 @@ export const testGridDescriptor: GridDescriptor<TestRow, number, number> = {
// `GridGrouping.order`.
order: (region) => region,
label: (region) => REGIONS[region]?.label ?? String(region),
// The header reads its colour from the very cell that draws the column, so
// a group's header text and that group's badges cannot drift apart.
color: (region) => regionCell.colorOf(region),

// No `color` here. The grid groups by a `dg:enum` column, so the engine
// takes the header's colour from that column's own choices and a group's
// header text cannot drift from its badges.

// The whole reason group headers left the canvas. Each region gets a
// DIFFERENT set of components, which is what the hook is for.
Expand All @@ -94,17 +157,10 @@ export const testGridDescriptor: GridDescriptor<TestRow, number, number> = {
fetchCount: fetchTestRowCount,
fetchRow: fetchTestRow,
updateRow: updateTestRow,
loadColumns: columnsAdapter.loadColumns,
saveColumns: columnsAdapter.saveColumns,
},
cells: testGridCells,
};

export const testGrid = createGridInstance(testGridDescriptor, {
// The engine ships no listener middleware of its own — column persistence
// runs on the host app's. See `src/listener.ts`.
startListening: startAppListening,
});
export const testGrid = createGridInstance(testGridDescriptor);

/** The shape this grid's slice adds to the demo store, mounted by `store.ts`. */
export type TestGridState = GridSliceState<number, number>;
Loading
Loading