Skip to content

Data grid

function

declare function DataGrid<R>(props: DataGridProps<R>): JSX.Element;

A table of a query document’s rows, sortable by header, filtered from a filter row, and paged.

<DataGrid doc={ foos } columns={ ["name", "num", "owner", "created"] }/>

Each column is derived from its field path and the schema, see resolveColumn(). Given a working set, the grid shows its drafts and marks rows and cells by what happened to them, see useGridRows(). The grid owns a component of the document’s condition, see useFilters(), so a search form writing a component of its own filters the same rows. When an update() fails, the grid’s own or anyone else’s, it says so above the rows, which stay the ones from before.

Composed from resolveColumn(), useFilters(), useGridRows(), SortHeader, FilterInput and Pager and nothing else. Where it doesn’t fit, copying it into the application and changing it is a reasonable answer.

type

type DataGridProps<R> = {
/**
* Query document snapshot to show.
*/
doc: GridDocument<R>;
/**
* The columns: field paths of the rows, or objects stating what differs from the defaults derived from one.
*/
columns: readonly NoInfer<GridColumn<R>>[];
/**
* Component id of the grid's filters in the document's condition. Default "grid"; a view sharing the condition
* with another owner gives each owner its own.
*/
id?: string;
/**
* Working set the rows are edited in. The grid then shows the drafts, the rows created in the working set on the
* first page, and what has happened to each row and cell, see useGridRows().
*/
workingSet?: WorkingSet | null;
/**
* Whether other people's writes show as they happen, see useGridRows(). Without a working set, the grid offers
* to reload once a row it shows changed.
*/
watch?: boolean;
/**
* Classes of a row's element.
*/
rowClassName?: (row: R) => string | undefined;
/**
* Tooltip of a column's header cell, e.g. a description of the sort field its arrow stands for. What that says
* and how it's put is the application's; `matchSort()` and `matchSortPart()` find the sort field. A column with a
* `title` of its own keeps that.
*/
headerTitle?: (column: ResolvedColumn) => string | undefined;
/**
* Key of the row to highlight, e.g. the one open in a detail pane. Rows are keyed as `rowKey()` keys them.
*/
highlighted?: string | null;
/**
* Page sizes the pager offers, see usePagination().
*/
pageSizes?: readonly number[];
/**
* Added to the grid's own classes, e.g. `qlive-grid-striped`.
*/
className?: string;
};

interface

interface GridDocument<R> extends PageableDocument, FilterableDocument

What DataGrid needs of a query document: its type, config, rows, row count and update(), and its error if it has one. A snapshot from useInjection() or useQueryDocument() is one as long as its query selects type, config, rows and rowCount.

rows: readonly R[];
error?: Error | null;

Why the last update() failed, see QueryDocument#error. The grid shows it above the rows, which are still the ones from before.

type

type GridColumn<R = any> = FieldPath<R> | DataGridColumn<R>;

A column as a grid takes it: a field path, which means {field: path}, or the object form.

interface

interface DataGridColumn<R = any>

A column where the defaults derived from its field path won’t do. Everything but the field is optional and overrides what is derived.

field?: FieldPath<R>;

The field path. Without it the column is an action or computed column and needs render.

heading?: ReactNode;

The heading, in place of the i18n() of Type.field.

title?: string | ((column: ResolvedColumn<R>) => string | undefined);

Tooltip of the header cell, fixed or from the resolved column, e.g. to add what it sorts by. The grid’s headerTitle titles only the columns without one.

render?(row: R): unknown;

The cell content. An element is used as it is, any other value is formatted like a value of the column’s field.

sort?: FieldExpression | false;

What the column sorts by, a field path or a FilterDSL expression without a direction, in place of the field; false for a column that doesn’t sort.

filter?: ColumnFilter<any> | false;

The column’s filter in place of the one derived from the field’s type; false for a column without one. A computed column has none unless it names one, and filters by what it sorts by: between on a sum, where the column sorts by the sum.

className?: string | ((row: R) => string | undefined);

Added to the classes of the column’s cells, fixed or per row.

nowrap?: boolean;

Keeps the cell content on one line.

minWidth?: string;

CSS width the column doesn’t get narrower than

maxWidth?: string;

CSS width the column doesn’t get wider than

type

type FieldPath<T, D extends number = 4> = [D] extends [never] ? never : T extends object ? { [K in keyof T & string]-?: PathOf<K, NonNullable<T[K]>, D>; }[keyof T & string] : never;

The field paths of a row type: its fields, and the fields of the to-one relations it holds, as "owner.login", down to a depth of 4. Lists drop out, since a column shows one value per row.

Taken from the result type of the grid’s query, a path is only valid if the query selects it, so a typo and a field the query doesn’t select are both compile errors.

function

declare function resolveColumn<R = any>(type: string, column: NoInfer<GridColumn<R>>): ResolvedColumn<R>;

Completes a grid column: the heading, the display, the sort key and the filter, derived from the field path and the schema where the column doesn’t state them.

  • A scalar path shows the value formatted for its type (see formatValue()), sorts by the field, and filters by type: containsIgnoreCase for String, a yes/no select for Boolean, eq for Int, Short, Byte and Float, a date range for Date and Timestamp. Other types have no default filter.
  • A to-one relation path ("owner") stands for the related row, named by the target type’s nameFields meta: it shows the name fields, most significant first, and sorts and filters by the first one. The query has to select them (owner { id login }). A filter that picks the related row, like pick(), filters the foreign key instead.

The heading is the i18n() of the owning type and the field, "Foo.name", "AppUser.login", so a type’s fields are labeled once for every grid showing them.

A path the schema doesn’t have, a list, a non-scalar that isn’t a to-one relation, or a relation whose type has no name fields where the column needs them is an error naming the column.

Parameters

type row type of the grid’s document
column the column as given

interface

interface ResolvedColumn<R = any>

A column with everything derived that can be: what DataGrid renders, and what a hand-written table built on the same hooks gets.

field: string | null;

the field path, null for a computed column

heading: ReactNode;
title?: string;

tooltip of the header cell, if the column has one

render(row: R): ReactNode;

the cell content of a row

sort: FieldExpression | null;

what the column sorts by, null if it doesn’t sort

filter: FilterColumn | null;

The column’s filter and the field it filters, null if it has none. For a relation column that is its first name field, "owner.login", not the relation, or its foreign key, "ownerId", for a filter that picks the related row (ColumnFilter.key). For a computed column it is the sort key. The filter is labeled with the heading where that is text, so its inputs are called what the header says.

className(row: R): string | undefined;

classes of the column’s cells for a row

statusField: string | null;

The field of the row whose working set status the cells show: the field itself for a field of the row, the foreign key for a relation of it. null for a field of a related row and for a computed column.

nowrap: boolean;
minWidth?: string;
maxWidth?: string;

function

declare function rowKey(type: string, row: any): string;

The key of a row: its primary key, from the uniqueKeys meta of the type, or its id for a type without one. The values of a key of several fields are joined with commas.

Parameters

type row type
row the row

function

declare function useGridRows<R>(doc: PageableDocument & {
rows: readonly R[];
}, options?: GridRowsOptions): GridRows<R>;

The rows a list shows, and what has happened to each of them and their fields.

const {rows, status, fieldClass} = useGridRows(foos, {workingSet: ws, watch: true})

Without a working set the rows are the document’s own. With one they are its drafts, so a row being edited elsewhere, in a detail pane or a dialog, shows the edits before they are saved; the rows created in the working set are listed first on the first page. Which of them is decided the way a query decides which rows it returns: when the condition changes, the created rows it matches then show, evaluated in the browser as conditionPredicate() has it, and so does every row created after. A row created under a filter shows although it is still empty, and neither it nor a row of the query leaves the list because an edit made it stop matching. The document is registered with the working set here, so its rows can be edited without doing that first.

The calling component re-renders with every change to the working set. DataGrid is this plus markup; a table of its own gets the same through this hook.

Rules of hooks apply.

Parameters

doc query document snapshot
options working set and watching, see GridRowsOptions

interface

interface GridRows<R>

The rows of a list and what has happened to them.

rows: readonly R[];

The rows to show. Without a working set, the document’s rows. With one, the drafts of them, preceded on the first page by the rows created in it that the document’s condition matched when it was set, and the rows created since.

status(row: R): GridRowStatus;

what has happened to a row

fieldStatus(row: R, field: string): MergeFieldStatus;

What has happened to a field of a row. A new row’s fields are unchanged: the row says it all.

fieldClass(row: R, field: string): string;

What has happened to a field of a row, as the class a cell or input carries (see MergeField.className), empty for an unchanged field: fieldClassName(fieldStatus(row, field)).

stale: boolean;

true once somebody else changed a row shown, when watching the document rather than a working set. The document’s update({}) reads the rows again.

interface

interface GridRowsOptions

Options for useGridRows,

workingSet?: WorkingSet | null;

The working set the rows are edited in. The rows are then its drafts, so unsaved edits show in the list, and the rows created in it show on the first page, those the document’s condition matches.

watch?: boolean;

Whether other people’s writes to the rows show as they happen: through the working set if there is one, see useWorkingSet(), through useDocumentWatch() on the document otherwise. Default false.

type

type GridRowStatus = "gone" | "deleted" | "conflict" | "new" | "changed" | "remoteChanged" | "unchanged";

What has happened to a row, the most pressing first:

  • gone – somebody else deleted it
  • deleted – marked for deletion, deleted by the next merge
  • conflict – a field of it both writes changed, and nobody decided which value wins
  • new – created in the working set and not saved yet
  • changed – changed in the working set
  • remoteChanged – somebody else changed it, and the working set’s user didn’t
  • unchanged – none of that