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:
bind | Edits |
|---|---|
{ 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)} />;Last updated on