Grid columns
A Grid column is one vertical slice of the Grid pane. It names a Field and carries presentation
only: header, width, alignment, and the cell's renderer. The Gantt's gridColumns option lists the
columns in display order.
This page shows the two ways to write a column: a plain column object, and the column helper. See
docs/05-consumer-api.md for the rest of the Gantt options, and
CONTEXT.md for the terms.
Every example below typechecks against the built package types on each CI run
(scripts/check-doc-examples.mjs).
A column is a plain object
A column is plain data. A bare Field key is the short form. It takes its header and width from the
Field's own column defaults.
import { Gantt } from 'freegantt';
new Gantt({
container,
dataset,
gridColumns: ['name', 'start', { field: 'cost', header: 'Budget', width: 96, align: 'end' }],
});
A plain object is a complete column. You never need the helper to show a column.
A renderer reads the value before formatting
A columnRenderer paints the column's cells. It receives two readings of one cell:
valueis the string the grid paints, from the Field's ownformatValue.fieldValueis the same Field value before formatting — whatentry.read(field)answers.
In a plain column object, fieldValue is unknown. A column object cannot type its renderer from
its own field key, so a renderer that reads fieldValue must check or cast the value.
import { Gantt } from 'freegantt';
new Gantt({
container,
dataset,
gridColumns: [
// Reads `value` only, so no type is necessary.
{ field: 'owner', columnRenderer: ({ value }) => ({ text: value.toUpperCase() }) },
// `fieldValue` is `unknown` here, so the renderer checks it.
{
field: 'cost',
columnRenderer: ({ fieldValue }) => ({ text: typeof fieldValue === 'number' ? fieldValue.toFixed(2) : '' }),
},
],
});
The column helper types the renderer
createGridColumnHelper(dataset) reads the props type from your Dataset. Its column(field, options)
types the renderer from that key: fieldValue has the type entry.read(field) answers. This
includes an inline renderer, with no annotation.
import { Gantt, createGridColumnHelper } from 'freegantt';
const columnHelper = createGridColumnHelper(dataset);
new Gantt({
container,
dataset,
gridColumns: [
'name',
// fieldValue: Instant | undefined
columnHelper.column('start', {
columnRenderer: ({ fieldValue }) => ({ text: fieldValue?.toString() ?? '' }),
}),
// fieldValue: number | undefined
columnHelper.column('cost', {
header: 'Budget',
align: 'end',
columnRenderer: ({ fieldValue }) => ({ text: fieldValue?.toFixed(2) ?? '' }),
}),
],
});
The helper is optional. It adds types only. column() returns the same plain object you can write
by hand, so the two forms mix freely in one gridColumns list:
import { createGridColumnHelper } from 'freegantt';
const columnHelper = createGridColumnHelper(dataset);
const typed = columnHelper.column('cost', { header: 'Budget', width: 96 });
// OR
const plain = { field: 'cost', header: 'Budget', width: 96 };
fieldValue is undefined on a row with no Entry, such as a group row, and on an Entry with no
value for the Field. So its type always includes undefined.
A named renderer
A renderer that you write once and use in more than one place takes ColumnRendererContext<TValue>.
Give it to columnHelper.column(). A plain column object does not accept it, because a plain
object's renderer reads unknown.
import { createGridColumnHelper, formatDate, type ColumnRendererContext, type Instant } from 'freegantt';
const columnHelper = createGridColumnHelper(dataset);
function dateCell({ fieldValue }: ColumnRendererContext<Instant>) {
return {
text: fieldValue === undefined ? '' : formatDate(fieldValue, { timeZone: dataset.timeZone, locale: 'en-US' }),
};
}
const gridColumns = [
columnHelper.column('start', { columnRenderer: dateCell }),
columnHelper.column('end', { columnRenderer: dateCell }),
];
A renderer for any value, such as the shipped meter() and image(), fits every key.
A date column's own formatter
value above always comes from the Field's formatValue, not the renderer. start and end
are ordinary date Fields, so either takes any formatValue a consumer writes — including the
two shipped ones:
formatDateTime— the stored moment, date and clock time. Thedatetype's default.formatInclusiveDate— the last day a span covers, date only. Readsend - 1 ms, unless the span is zero-length (end === start), which shows that moment unchanged; works with nostarttoo. Coreendsets this as its default, so a Finish column already shows a date, not a half-open boundary.
Set a formatter through fields, on the Dataset, not through columnRenderer:
import { Dataset, formatDateTime } from 'freegantt';
new Dataset({
timeZone: 'Europe/Warsaw',
entries: [ /* … */ ],
fields: [{ key: 'end', formatValue: formatDateTime }], // End shows the stored moment with clock time
});
A custom format builds on the public dateFormatter and lastCoveredInstant:
import { dateFormatter, lastCoveredInstant, type FormatContext, type Instant } from 'freegantt';
const compactDay = dateFormatter({ day: '2-digit', month: 'short' });
function compactFinish(
value: unknown,
ctx: FormatContext,
entry: { readonly start?: Instant | undefined },
): string {
if (value === undefined || value === null) return '';
return compactDay(lastCoveredInstant({ start: entry.start, end: value as Instant }), ctx);
}
A formatValue set on start or end this way reaches the row's tooltip too, not only the grid
cell — both read through the same Field text.
A duration column
duration's fieldValue is a Duration | undefined, and value is already the formatted text
(12 d). Change that text through fields on the Dataset, the same as for start or end —
not through columnRenderer. A renderer reads fieldValue for a decision, not to recompute the
text:
import { createGridColumnHelper, MS, type ColumnRendererContext, type Duration } from 'freegantt';
const columnHelper = createGridColumnHelper(dataset);
function durationCell({ value, fieldValue }: ColumnRendererContext<Duration>) {
const long = fieldValue !== undefined && fieldValue.value > 5 * MS.DAY;
return { text: value, className: long ? 'long-task' : undefined };
}
const gridColumns = [columnHelper.column('duration', { columnRenderer: durationCell })];
What the helper checks
- A renderer for the wrong type does not compile. A
ColumnRendererContext<number>renderer on theownerkey, astring, is a type error. - A key that is neither a core key nor a key of your props type does not compile.
widthandflextogether do not compile, the same as in a plain object.
When the helper gives no benefit
The helper types a key only when it knows the key's type. Use a plain column object for a key it does not know:
- A plugin's own Field, such as
scheduling:progress. Your props type does not name it. - A computed Field that your props type does not name. A
computeField adds a key, but not a type. - An untyped Dataset. With no props type, only the core keys have a type.
- A renderer that reads only
value. It gets nothing from the type.
import { meter } from 'freegantt';
const gridColumns = [{ field: 'scheduling:progress', header: 'Done', columnRenderer: meter() }];
A Field's text off the Grid
gantt.formatFieldValue(entry, key) gives a Field's text with no column at all — a hidden
column, a Field that never had one, or a Field you read for a status line or a CSV export. It
reads through the same door a Grid cell does, so the two texts never disagree.
Reading columns back
gantt.gridColumns and the gridColumnsChange event give back plain column objects. A renderer
reads fieldValue as unknown there. So you can read the list, change it, and write it back:
gantt.gridColumns = [...gantt.gridColumns, 'cost'];
Why a helper, and not a typed column object
A typed column object needs one type per key, in one union: a start column, a cost column, and
so on. That union also needs a case for a key it does not know, such as a plugin's own key. A
known key such as start then matches two cases. TypeScript does not type an inline renderer when
two cases match, so the renderer's parameter becomes an implicit any.
A function call does not have this problem. The key is an argument, so TypeScript knows it before it reads the renderer. Other libraries use the same approach:
- TanStack Table:
createColumnHelper()andcolumnHelper.accessor(key, …). Its column definitions are plain objects too, and each helper call has a plain-object equivalent. - Vite:
defineConfig({ … }). A plain exported object is also a valid config. The helper only adds types.