Field types

Define a kind of value once — how it is drawn, edited, read, sorted, filtered and worked out — and use it everywhere.

A field type is one definition of a kind of value: money with its currency, a quantity with its unit, a link to a vendor, a line total worked out from a quantity and a price. A column names it with type, and every place the value appears follows it: the cell, the record inspector, cards, forms, export, copy and paste, sorting, filtering and grouping.

import { defineFieldType, type ColumnDef } from '@witbybit/wit-grid-react';

interface Money {
	amount: number;
	currency: string;
}

/** An amount with its currency, typed as "12.50 EUR". */
const money = defineFieldType({
	id: 'money',
	label: 'Money',
	hint: 'An amount with its currency',
	group: 'Numbers',
	create: (settings: { currency?: string } = {}) => ({
		schema: { kind: 'currency' as const },
		format: ({ value }: { value: unknown }) => {
			const money = value as Money | null;
			return money ? `${money.amount.toFixed(2)} ${money.currency}` : '';
		},
		parse: ({ text }: { text: string }) => {
			const [amount, currency] = text.trim().split(/\s+/);
			const n = Number(amount);
			return Number.isFinite(n) ? { amount: n, currency: currency ?? settings.currency ?? 'USD' } : undefined;
		},
		sortValue: (value: unknown) => (value as Money | null)?.amount ?? null,
	}),
});

const columns: ColumnDef<{ id: string; total: Money }>[] = [{ field: 'total', header: 'Total', type: money({ currency: 'EUR' }) }];

create returns column settings — schema, renderer, cellEditor, format, parse, sortValue, filterDef, display, canEdit and the rest a column takes — and the column's own settings win over them. A setting the column leaves unset inherits the type's; to do without one of the type's settings, the column names it in reset ({ field: 'vendor', type: selectType(vendors), reset: ['renderer'], display: 'vendor' }: the type's renderer is not used, so the column's display draws). A renderer or an editor can be the grid's DOM ones or your framework's components (React: { kind: 'react', component }).

The grid's own types are made the same way: textType, numberType, currencyType, percentType, dateType, dateTimeType, selectType, comboboxType, multiSelectType, tagsType, personType, linkedRecordType, ratingType, progressType, checkboxType, switchType, segmentedType, colorType, longTextType, dateRangeType, timelineType, cascadeType, sparklineType, urlType and emailType (see Columns).

Values worked out from others

A type can calculate its value from the record's other fields with derive. It is worked out when a write is prepared: a write that changes a field it depends on writes it too, as part of the same change. So the cell, a draft's preview, the rules, totals and what is saved all hold the same number, and undo takes both back together.

import { defineFieldType } from '@witbybit/wit-grid-react';

const lineTotal = defineFieldType({
	id: 'lineTotal',
	create: () => ({
		canEdit: () => false,
		derive: { dependsOn: ['qty', 'price'], value: (row: { qty: number; price: number }) => row.qty * row.price },
	}),
});

// { field: 'total', header: 'Total', type: lineTotal() }

Calculated fields are worked out in the order they depend on each other, whatever the column order: a gross that depends on total and tax is worked out after both. Calculated fields that depend on each other in a cycle throw when the columns are set, naming the fields. A calculation that throws refuses the write, naming the field and the error: a stale value is never left standing. A record a write adds (a quick-added line, newRows, a draft that adds one) gets its calculated fields worked out, and a draft's proposal shows them in added.

Records loaded from your server keep the values they arrive with: a derived value is the grid's to keep current as people edit, not to recompute on load.

Changes made apart

Two changes to the same value made apart (here and on the server, or here and under a draft) are a conflict for a person to settle — unless the type knows how to join them. merge(base, mine, theirs) returns the joined value, or undefined when they truly collide. Collections merge row by row this way: a teammate's change to one line and yours to another both stand. A type that can say which parts collided gives collisions(base, mine, theirs) ([{ id, field }]): the conflict on the field then names them (parts; a draft's collisions).

Types people pick for their own fields

Hand the grid your types in fields.types: a field someone defines can name one (type: 'money'), and those with a label are offered in the add-field picker, grouped by group. api.getFieldTypes() lists what the picker offers.

import { Grid, defineFieldType, type ColumnDef, type GridFieldStore } from '@witbybit/wit-grid-react';

declare const money: ReturnType<typeof defineFieldType<[settings?: { currency?: string }]>>;
declare const store: GridFieldStore;
declare const rows: { id: string }[];
declare const columns: ColumnDef<{ id: string }>[];

<Grid rows={rows} columns={columns} fields={{ types: [money], store }} />;

fromField(def, context) says what settings a field definition makes the type with; by default its settings and options.

A field's own control

createFieldControl(api, field, { bind }) — <FieldControl> in React — draws a field the way its column draws it and edits it with the column's own editor, outside the table: a form, a dialog, a side panel. It is bound to what it edits:

bindEdits
{ record: id }The record: each edit committed as the person's edit (rules, capabilities, undo, saving)
{ draft }A record's draft (api.draft(id)): each edit sets it; nothing is written until it is applied
{ value, onChange }A value of its own (a dialog's field before there is a record)

Bound to a record or a draft it follows them: a change made anywhere redraws it. It asks the record's rules with the record as proposed — a draft's other values included — so a stage drafted to "confirmed" locks the fields its canEdit locks, and canShow hides a field that does not apply. The record inspector's fields are field controls.

import { FieldControl, type GridApi } from '@witbybit/wit-grid-react';

declare const api: GridApi<{ id: string; approver: string }>;
declare const invoiceId: string;

<FieldControl field='approver' draft={api.draft(invoiceId)} />;
Edit on GitHub

Last updated on

On this page