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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Changelog

## 0.1.0-beta.0

First public beta. The API may still change before 0.1.0.

### Added

- `DataGrid` component: fixed-height rows virtualized with FlashList, sticky header, and columns pinned to the left.
- Sorting: tap a header to cycle ascending, descending, unsorted. Stable, natural string order, empty values last. Custom `compare` per column.
- Selection: `single` and `multiple`, with a pinned checkbox column, select-all, and controlled or uncontrolled state.
- Search and filters: `searchText` that ignores case and accents, plus `text`, `values`, `range` and `custom` column filters. The search index is built on the first search and reused until `data` or `columns` change.
- Server-driven data: `manualSorting`, `manualFiltering`, `loading`, `loadingMore`, pull-to-refresh, `onEndReached`, and `error` with retry.
- Custom cells through `renderCell`, display text through `format`.
- Light and dark themes, with every color overridable, following the device by default.
- Row-level accessibility labels in visual column order, with a selection action.
- Exported helpers: `sortRows`, `nextSort`, `filterRows`, `buildSearchIndex`, `matchesColumnFilter`, `getCellText`, `getCellValue`, `lightTheme`, `darkTheme`.

### Known limitations

- Every mounted row renders every column. Rendering only visible columns is planned for wide tables.
- Sorting, filtering and key generation process the whole dataset in JS.
- No column resizing or inline editing yet.
- No native horizontal scrollbar or overscroll bounce: sideways scrolling is driven by a pan gesture so the header and pinned columns stay in sync ([#3](https://github.com/ahmdshrif/react-native-datagrid/issues/3)).
- Performance has been measured on the iOS simulator and an Android emulator only. Physical low-end Android devices are not measured yet.
258 changes: 122 additions & 136 deletions README.md

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Releasing

## Beta

1. `npm login` (once per machine).
2. Make sure `main` is green in CI and your working tree is clean.
3. Bump the version in `package.json` if needed, for example `0.1.0-beta.1`, and add a `CHANGELOG.md` entry.
4. `yarn prepare` — builds `lib/` with bob.
5. `npm publish` — `publishConfig.tag` is `beta`, so it will **not** become `latest`.
6. `git tag v0.1.0-beta.0 && git push --tags`.

Install instructions for testers: `npm install react-native-datagrid@beta`.

## Stable

Same steps, with `npm publish --tag latest`, after the beta has been confirmed on physical devices.

## Checklist before publishing

- `yarn lint`, `yarn typecheck`, `yarn test` pass.
- `npm pack --dry-run` lists only `src/`, `lib/`, `README.md`, `LICENSE` and `package.json`.
- The example app builds and runs in Release on both platforms.
- A clean install into a fresh app renders the grid (see `docs/spike-results.md` for the method).
104 changes: 104 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# API

## `<DataGrid />`

The grid fills its parent (`flex: 1`), so give the parent a height.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data` | `readonly T[]` | required | Rows. |
| `columns` | `DataGridColumn<T>[]` | required | Column definitions. Memoize them, or every row re-renders. |
| `keyExtractor` | `(row, index) => string` | required | Unique key per row. Use a stable ID from the row, not the index. |
| `rowHeight` | `number` | `44` | Fixed row height. |
| `headerHeight` | `number` | `40` | Header height. |
| `striped` | `boolean` | `true` | Alternate row backgrounds. |
| `sort` | `SortState \| null` | | Controlled sort. |
| `defaultSort` | `SortState \| null` | `null` | Initial sort when uncontrolled. |
| `onSortChange` | `(sort) => void` | | Called when a header is tapped. |
| `manualSorting` | `boolean` | `false` | Keep the order of `data`; the header still shows `sort` and taps still call `onSortChange`. |
| `searchText` | `string` | | Show rows whose searchable columns contain this text. |
| `columnFilters` | `ColumnFilters<T>` | | Show rows matching every filter, by column key. |
| `manualFiltering` | `boolean` | `false` | Show `data` without applying `searchText` or `columnFilters`. |
| `onFilteredCountChange` | `(count) => void` | | Number of rows left after filtering. |
| `selectionMode` | `'none' \| 'single' \| 'multiple'` | `'none'` | `multiple` adds a pinned checkbox column. |
| `selectedKeys` | `readonly string[]` | | Controlled selection. |
| `defaultSelectedKeys` | `readonly string[]` | `[]` | Initial selection when uncontrolled. |
| `onSelectionChange` | `(keys: string[]) => void` | | Called with all selected keys. |
| `onRowPress` | `(row, index) => void` | | Row tap. In `multiple` mode, setting this makes row taps call it instead of toggling selection. |
| `loading` | `boolean` | `false` | First load. Shows a spinner only while there are no rows. |
| `loadingText` | `string` | `'Loading'` | Text under the first-load spinner. |
| `loadingMore` | `boolean` | `false` | Shows a spinner below the last row. |
| `refreshing` | `boolean` | | Pull-to-refresh state. |
| `onRefresh` | `() => void` | | Enables pull-to-refresh. |
| `onEndReached` | `() => void` | | Called near the end of the list, to load the next page. |
| `onEndReachedThreshold` | `number` | `0.5` | How far from the end `onEndReached` fires, in visible list lengths. |
| `error` | `string \| null` | | Error message. Replaces the empty state when there are no rows, otherwise shows below the last row. |
| `onRetry` | `() => void` | | Shows a Retry button next to `error`. |
| `colorScheme` | `'auto' \| 'light' \| 'dark'` | `'auto'` | `auto` follows the device. |
| `theme` | `Partial<DataGridTheme>` | | Override theme tokens. See [theming](theming.md). |
| `emptyText` | `string` | `'No rows'` | Shown when there are no rows to show. |
| `style` | `StyleProp<ViewStyle>` | | Container style. |
| `testID` | `string` | | Also sets `${testID}-list` and `${testID}-horizontal`. |

## `DataGridColumn<T>`

| Option | Type | Description |
| --- | --- | --- |
| `key` | `string` | Unique id. Also reads `row[key]` when `getValue` is not set. |
| `title` | `string` | Header text. |
| `width` | `number` | Width in points. |
| `pinned` | `'left'` | Keep the column visible while scrolling sideways. Pinned columns move to the front. |
| `align` | `'left' \| 'center' \| 'right'` | Cell alignment. Default `left`. Right-aligned cells use tabular figures. |
| `sortable` | `boolean` | Let users sort by tapping the header. |
| `getValue` | `(row) => unknown` | Read the value used for display and sorting. |
| `compare` | `(a, b) => number` | Custom ascending comparator. Reversed automatically for descending. |
| `format` | `(value, row) => string` | Display text. Ignored when `renderCell` is set. |
| `renderCell` | `(info) => ReactNode` | Custom cell content. `info` is `{ row, rowIndex, value, column, selected }`. |
| `searchable` | `boolean` | Include in `searchText` matching. Default `true`. |
| `getSearchText` | `(row) => string` | Text used for search. Defaults to the shown text. |

## Sorting

Tapping a sortable header cycles ascending → descending → unsorted.

Sorting is stable, and empty values (`null`, `undefined`) always go last, in both directions. Numbers compare numerically, dates by time, booleans false-first, and strings with `Intl.Collator` using natural number order and no case or accent sensitivity, so `Item 2` comes before `Item 10`.

```tsx
const [sort, setSort] = useState<SortState | null>({
columnKey: 'amount',
direction: 'desc',
});

<DataGrid sort={sort} onSortChange={setSort} /* ... */ />;
```

## Filters

| Filter | Matches |
| --- | --- |
| `{ type: 'text', value }` | Shown cell text contains `value`, ignoring case and accents. |
| `{ type: 'values', values }` | Cell value equals one of `values` (dates compare by time). An empty list matches everything. |
| `{ type: 'range', min?, max? }` | Number or date between `min` and `max`, inclusive. Cells that aren't valid numbers or dates never match; invalid bounds are ignored. |
| `{ type: 'custom', test }` | `test(row)` returns true. |

`searchText` matches against every column with `searchable !== false`, using what the cell shows (`format` output), so `$1,114.00` matches a search for `1,114`.

The grid has no built-in filter controls; render your own search box and chips, as the example app does.

## Exported helpers

These are plain functions, usable without the component:

| Function | Purpose |
| --- | --- |
| `sortRows(data, columns, sort)` | Returns a sorted copy, or `data` itself when nothing sorts. |
| `nextSort(current, columnKey)` | The header-tap cycle: ascending → descending → `null`. |
| `filterRows(data, columns, options)` | Applies `searchText` and `columnFilters`. |
| `buildSearchIndex(data, columns)` | One searchable string per row; pass it back to `filterRows` to skip rebuilding. |
| `matchesColumnFilter(row, column, filter)` | Tests one filter against one row. |
| `getCellValue(row, column)` / `getCellText(row, column)` | The value and the shown text for a cell. |
| `lightTheme` / `darkTheme` | The built-in theme token sets. |

## Types

`DataGridProps`, `DataGridColumn`, `CellRenderInfo`, `CellAlign`, `SortState`, `SortDirection`, `SelectionMode`, `ColumnFilter`, `ColumnFilters`, `DataGridTheme`.
Binary file added docs/media/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions docs/server-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Server-driven data

When a server sorts, filters or pages the data, turn off local processing and let the grid report what the user asked for. Your app owns fetching, pagination, retries and cancellation; the grid owns the states it shows.

```tsx
<DataGrid
data={rows}
columns={columns}
keyExtractor={(row) => row.id}
sort={sort}
onSortChange={setSort} // refetch page 1 with the new sort
manualSorting
manualFiltering
loading={isFirstLoad} // spinner only while there are no rows
loadingMore={isLoadingNextPage} // footer spinner, rows stay visible
refreshing={isRefreshing}
onRefresh={refetchFirstPage}
onEndReached={loadNextPage}
error={errorMessage}
onRetry={retryLastRequest}
/>
```

## What each flag means

| Prop | Behaviour |
| --- | --- |
| `manualSorting` | The grid keeps the order of `data`. The header still shows the sort arrow and taps still call `onSortChange`, so you can refetch. |
| `manualFiltering` | The grid shows `data` as it is. `searchText` and `columnFilters` are ignored locally; use them to build your request. |
| `loading` | Shown **only when there are no rows**. When rows are already on screen, they stay visible while the next result loads. |
| `loadingMore` | A spinner below the last row. |
| `error` | Replaces the empty state when there are no rows; otherwise appears below the last row so the loaded rows stay usable. |

## States to handle

- **First load:** `loading` true, no rows. The grid shows a spinner and `loadingText`.
- **Changing sort or filters with rows on screen:** keep the old rows, set `loading` true. The grid keeps showing them, so the screen doesn't flash empty.
- **Loading a page:** `loadingMore` true. Do nothing else; the rows stay put.
- **Failure:** set `error`, and `onRetry` to repeat the last request. Keep the rows you already have.
- **Empty result:** clear `loading` and `error` and pass an empty array; the grid shows `emptyText`.

## Cancelling in-flight requests

Sorting and typing produce overlapping requests. Cancel the previous one so a slow response can't overwrite a newer one:

```tsx
const controller = useRef<AbortController | null>(null);

const load = useCallback(async (page: number) => {
controller.current?.abort();
const current = new AbortController();
controller.current = current;
try {
const result = await fetchRows({ page, sort, search, signal: current.signal });
setRows((prev) => (page === 0 ? result.rows : [...prev, ...result.rows]));
} catch (e) {
if (!current.signal.aborted) setError(String(e));
}
}, [sort, search]);
```

## Known rough edge

After a new sort or filter, the list keeps its scroll position, so a deep scroll can call `onEndReached` immediately. A scroll-reset option is on the roadmap. Until then, you can remount the grid with a `key` that changes with the query.

A complete working version of all of this, with a fake paged API and a switch that makes the next request fail, is in the example app's **Server** tab: [`example/src/ServerDemo.tsx`](../example/src/ServerDemo.tsx).
67 changes: 67 additions & 0 deletions docs/theming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Theming

The grid ships light and dark themes and follows the device by default. Override any token, or force a scheme.

```tsx
<DataGrid
colorScheme="dark" // 'auto' (default), 'light' or 'dark'
theme={{ accent: '#7C4DFF', rowSelectedBackground: '#EEE7FF' }}
/* ... */
/>
```

## Tokens

| Token | Used for |
| --- | --- |
| `background` | Row and grid background. |
| `rowAlternateBackground` | Every second row when `striped`. |
| `rowSelectedBackground` | Selected rows. |
| `headerBackground` | Header row, including its pinned part. |
| `headerText` | Header labels. |
| `text` | Cell text and the active sorted header. |
| `mutedText` | Empty state, footer messages, unchecked checkbox border. |
| `border` | Row separators and header cell dividers. |
| `pinnedEdge` | The line marking the pinned column edge, and the header's bottom border. |
| `accent` | Sort arrow, checkboxes, spinners, Retry button. |
| `onAccent` | The checkmark inside a checked checkbox. |
| `fontSize` | Cell text size. Default `14`. |
| `headerFontSize` | Header label size. Default `11`. |
| `cellPaddingHorizontal` | Left and right padding inside every cell. Default `10`. |

## Building on the defaults

`lightTheme` and `darkTheme` are exported, so you can extend one instead of listing every token:

```tsx
import { darkTheme } from 'react-native-datagrid';

const brandDark = { ...darkTheme, accent: '#FFB300', onAccent: '#1A1A1A' };

<DataGrid colorScheme="dark" theme={brandDark} /* ... */ />;
```

## Matching your app's scheme

Pass `colorScheme` when your app has its own theme switch, so the grid follows your setting instead of the device:

```tsx
const { isDark } = useAppTheme();

<DataGrid colorScheme={isDark ? 'dark' : 'light'} /* ... */ />;
```

## Cell-level styling

Tokens cover the frame: backgrounds, borders, header and default cell text. For anything richer — status pills, coloured amounts, avatars — use `renderCell` and style it yourself:

```tsx
{
key: 'status',
title: 'Status',
width: 120,
renderCell: ({ value }) => <StatusPill status={value as Status} />,
}
```

Memoize the `columns` array, including any closures over your theme, or every row re-renders when the parent does.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-datagrid",
"version": "0.1.0",
"version": "0.1.0-beta.0",
"description": "Fast data grid for React Native: pinned columns, sticky header, virtualized rows, sorting and selection",
"main": "./lib/module/index.js",
"types": "./lib/typescript/src/index.d.ts",
Expand Down Expand Up @@ -65,7 +65,9 @@
},
"homepage": "https://github.com/ahmdshrif/react-native-datagrid#readme",
"publishConfig": {
"registry": "https://registry.npmjs.org/"
"registry": "https://registry.npmjs.org/",
"tag": "beta",
"access": "public"
},
"devDependencies": {
"@commitlint/config-conventional": "^21.2.0",
Expand Down
Loading