Column mapping
A widget should not depend on a user's exact column names. Instead, declare logical names your code uses; the user maps them to real columns through the section configuration UI.
Declare columns once
Pass columns to the provider:
<GristWidgetProvider
options={{
requiredAccess: "full",
columns: [
{ name: "Title", type: "Text" },
{ name: "Done", type: "Bool" },
{
name: "Tags",
type: "ChoiceList",
allowMultiple: true,
optional: true,
description: "Optional tags to filter rows.",
},
],
}}
>Field reference:
| Field | Notes |
|---|---|
name | Required. Must be a valid JS identifier. This is the name your code uses. |
title | Short label in the Grist mapping UI. |
description | Long help text in the mapping UI. |
type | Comma-separated allowed Grist column types. Defaults to "Any". |
optional | Default false. Required columns gate mappedRecord. |
allowMultiple | true lets the user map several real columns to one logical name. The corresponding mapping becomes string[]. |
strictType | true makes the type match exact (so "Any" only matches "Any"). |
You can also pass plain strings (columns: ["Title", "Done"]) when you don't need type hints.
Read mapped data
useGrist() exposes:
const w = useGrist<MyMappedRow>()
w.mappedRecord // logical-name view of the selected row (or null)
w.mappings // { Title: "Name", Done: "isDone", Tags: ["tagA","tagB"] }
w.recordsMappings // same for w.records
w.columnMappingStatus
// { ok: boolean, missing: string[], emptyMultiples: string[] }mappedRecord is null when any required column is unmapped. Never silently fall back to the raw record.
Typing
Type the SDK with two generics — your raw row shape (real column ids) and the mapped logical shape:
type RawTask = {
Name: string
isDone: boolean
tag_list: string[]
}
type MappedTask = {
Title: string
Done: boolean
Tags?: string[]
}
const w = useGrist<RawTask, MappedTask>()
w.record?.Name // typed
w.mappedRecord?.Title // typedSee Typing your rows for more.
Gate UI on missing mappings
if (!w.columnMappingStatus.ok) {
return (
<p>
Map these columns first: {w.columnMappingStatus.missing.join(", ")}
</p>
)
}The SDK also exposes getGristSdkAlertDescriptors(w) which returns ready-to-render descriptors for the same condition, so you can wire your own Alert UI:
const alerts = getGristSdkAlertDescriptors(w, {
columnMappingHint: "Open the widget settings (gear icon) to map columns.",
})
// alerts: Array<{ id, kind, ariaRole, message }>Write through the mapping
Writing back uses real column ids. Use w.mapBack(patch) to reverse the logical names:
async function toggle() {
const patch = w.mapBack({ Done: !w.mappedRecord!.Done })
await w.table.update({
id: w.record!.id,
fields: patch,
})
}mapBack:
- Uses
grist.mapColumnNamesBackwhen available. - Falls back to the local
mappingstable for single-mapped columns. - Skips
allowMultiplecolumns with a warning (you must choose which underlying column to write).
Resolve a single mapped column
For ad-hoc work, e.g. to set a Group by field:
const col = w.resolveMappedColumnId("Tags")
// returns the first real id when allowMultiple, or the logical name as a last-resort fallbackDynamic mappings
Mappings update as the user reconfigures the section. Subscribe via the same useGrist() — the slice that owns mappings re-renders by itself.
For one-off reads of the current mappings (e.g. as part of an action), use:
const current = await w.refreshMappings()To re-declare your widget's column requirements at runtime (rare, but supported for plugins like form builders):
await w.configure({
columns: [
{ name: "Title", type: "Text" },
{ name: "Body", type: "Text", optional: true },
],
})Allow-multiple recipes
const tagCols = (w.mappings.Tags as string[] | undefined) ?? []
const tags = w.record
? tagCols.flatMap((col) => {
const v = (w.record as Record<string, unknown>)[col]
return typeof v === "string" ? [v] : []
})
: []If you regularly read allowMultiple columns this way, consider modelling them with safeParseGristValues or per-column accessors in your widget code.