Fields people add
Let the people using the grid add, change and delete fields themselves — kept as plain JSON wherever your app keeps it.
Usually the app decides every column in code. With fields, the people using the grid can add their own: a
Risk select, a Cost in euros, a Reviewer. A field they add edits, sorts, filters, groups, charts, prints
and exports like any column, in the table and in every view.
import { Grid, createLocalStorageFieldStore, type ColumnDef } from '@witbybit/wit-grid-react';
interface Task {
id: string;
title: string;
status: string;
[field: string]: unknown;
}
declare const tasks: Task[];
declare const columns: ColumnDef<Task>[];
const fields = { store: createLocalStorageFieldStore('roadmap-fields') };
export function Roadmap() {
return <Grid rows={tasks} columns={columns} getRowId={(row) => row.id} fields={fields} />;
}Where people add them
- The + after the last column header adds a field at the end.
- The column header menu has Insert field left and Insert field right on every column, and Edit field… and Delete field… on the fields people added.
- In the workspace: New field in the Fields panel (a field made while a view is open shows on
its records at once), Add a field in the record inspector, and New field… in the command palette
(
field:new).
The field editor asks for a name (no two fields share one), a type, and what the type needs:
| Type | Settings |
|---|---|
| Text, Long text, Checkbox | — |
| Number, Percent | Decimals |
| Currency | Currency code, decimals |
| Progress, Rating | Stars (rating) |
| Single select, Multiple select, Tags | Options: label, colour, order (drag, or Alt + arrows); Enter adds the next |
| Person | Several people in a cell. People come from your person columns, or fields.people |
| Date, Date and time, Date range | — |
| Link, Email | — |
| Formula | The formula, and how its value shows (see below) |
| Link to records | What names a record, one or several per cell |
| Lookup, Rollup | The link to read through, the field, and (rollup) how to sum up |
Renaming an option keeps the value the cells hold, so records keep their choice. Deleting a field asks first; its column goes and its values stay in the rows.
Formula fields
A Formula field works its value out from the record's other fields:
{Budget} * 1.2
IF({Status} = "Done", "✓", DAYS({Due}, TODAY()) & " days left")
ROUND(AVERAGE({Q1}, {Q2}, {Q3}, {Q4}), 1)- Fields go in braces, by name. Type
{in the editor to pick one; the buttons under the formula put a field or a function in. Formulas are kept with the fields' keys, so renaming a field does not break them. - Text goes in quotes.
&joins text.=,!=(or<>),<,<=,>,>=compare (text in any case).+ - * /and%do numbers. A date plus a number is a later date, and a date minus a date is days. - The editor says what the formula gives back (a number, text, a date, yes or no) and works out the first records as you type. Show as draws it as a number, currency, percent, text, date or checkbox instead.
- A formula it cannot read shows #ERROR. A value it cannot work out for one record (dividing by zero, text where a number goes) stays blank, and the cell's tooltip says why. A formula cannot use itself, even through another formula.
Formula values sort, filter, group, chart and export like stored ones, and update as the fields they read change. They cannot be typed into. Change a formula into a stored type and its values are kept.
| Group | Functions |
|---|---|
| Logic | IF, AND, OR, NOT, ISBLANK, IFERROR, BLANK |
| Numbers | SUM, AVERAGE, MIN, MAX, COUNT, ROUND, FLOOR, CEILING, ABS, SQRT, POWER |
| Text | CONCAT, LEN, UPPER, LOWER, TRIM, LEFT, RIGHT, CONTAINS, SUBSTITUTE |
| Dates | TODAY, NOW, DAYS(end, start), DATEADD(date, n, "days"), YEAR, MONTH, DAY, WEEKDAY |
Links, lookups and rollups
A Link to records field connects records of the same grid: an epic to its tasks, a task to what it depends on.
- The cell shows the linked records as chips with their titles. Titles are read live, so renaming a record renames its
chips. Pressing a chip opens that record: in the workspace its inspector shows it; a plain grid scrolls to it and flashes it. Listen for
recordOpenRequestedto show it your own way. - The picker lists the grid's records and searches their titles. With
fields.createRecord, typing a title that isn't there offers Add “…”: a new record, linked at once. - Records are named by picks the field used as the title (default: the first text field). Several records in a cell is on by default.
- The value is the linked records' ids, so filters, grouping and charts work on it.
Through a link, two more types read the linked records:
- Lookup shows a field of the linked records, each value as that field shows it: their statuses, owners or due dates.
- Rollup sums them up: a count of records, or the sum, average, lowest or highest of a number field, or how many have it filled. Money stays money.
Both follow the linked records: change a task's budget and the epic's rollup changes with it. Changing a text field into a link finds the records its text names, by title.
import { Grid, type ColumnDef, type GridFieldsOptions } from '@witbybit/wit-grid-react';
interface Task {
id: string;
title: string;
[field: string]: unknown;
}
declare const tasks: Task[];
declare const columns: ColumnDef<Task>[];
const fields: GridFieldsOptions = {
initial: [
{ field: 'subtasks', name: 'Subtasks', type: 'link' },
{ field: 'cost', name: 'Total cost', type: 'rollup', settings: { link: 'subtasks', target: 'budget', rollup: 'sum' } },
],
// “Add …” in the picker makes a task (return it to have it linked at once).
createRecord: (values) => ({ id: crypto.randomUUID(), status: 'todo', ...values }),
};
export function Plan() {
return <Grid rows={tasks} columns={columns} getRowId={(row) => row.id} fields={fields} />;
}Changing a field's type
Change a field's type and the values already in it convert, as one undo step:
- Text to a number reads numbers the way people write them:
1,200,$45.50,25%(0.25). - Text to a select makes the options from the values, matching ones it has by name. The editor fills the options in before you save, ready to colour and order.
- A select to text writes the labels. Several options to one keeps the first.
- Text to a date reads dates, a date becomes a one-day range, and a range becomes its first day.
- Text to a person finds people by name. Text to a checkbox reads yes, true, x and ✓.
The editor says what will happen before you save: how many values convert, and which ones the new type can't read and will clear. Undo puts them back.
Where they are kept
A field is plain JSON:
import type { FieldDef } from '@witbybit/wit-grid-react';
const risk: FieldDef = {
field: 'fld_k3x9a2', // where the value lives in each row
name: 'Risk',
type: 'select',
options: [
{ value: 'low', label: 'Low', color: 'green' },
{ value: 'high', label: 'High', color: 'red' },
],
description: 'How likely it slips',
after: 'title', // where it was added
};fields.store loads the definitions when the grid starts and saves every change. Point it at your backend:
import type { GridFieldStore } from '@witbybit/wit-grid-react';
declare function getJSON(url: string): Promise<never>;
declare function putJSON(url: string, body: unknown): Promise<void>;
const store: GridFieldStore = {
load: () => getJSON('/api/boards/42/fields'),
save: (fields) => putJSON('/api/boards/42/fields', fields),
};Or hold them yourself: fields.initial, and the fieldsChanged event (or api.getFields()) to save.
Values live in the rows under each field's key and are written like any edit: validation, capabilities, undo and
the changed event all apply, so your app saves them the way it saves every cell.
| Option | Meaning |
|---|---|
initial | Definitions the app already has. |
store | load() / save(fields), sync or async. createLocalStorageFieldStore(key) for demos. |
editable | false keeps the fields but hides every way to change them. Default true. |
people | The people a person field offers. Default: the people of your own person columns. |
createRecord | Makes a record for a link picker's Add “…”, from { [titleField]: typed }. Return the row. |
The API
import type { GridApi } from '@witbybit/wit-grid-react';
declare const api: GridApi;
const key = api.addField({ name: 'Effort', type: 'rating', settings: { max: 5 }, after: 'status' });
api.updateField(key, { name: 'Effort (stars)' });
api.previewFieldChange(key, { type: 'number' }); // { changed, cleared, clearedSamples, addedOptions }
api.updateField(key, { type: 'number' }, { convertValues: true }); // converts the values, one undo step
api.getFields(); // the definitions, as JSON
api.isUserField(key); // true: one people added, not one of the app's columns
api.removeField(key);
api.canEditFields(); // the grid has `fields` and they are editablefieldColumn(def) turns a definition into the ColumnDef the grid uses, for code that renders fields elsewhere.
Your columns stay yours. The app's columns and the fields people add are kept apart: when your app passes
columns again (a React render), the added fields stay where people put them, and a field never replaces a column
of yours with the same key.
Last updated on