VitalGrid Fields Guide
This guide explains how to use and extend the field system in VitalGrid.
This guide explains how to use and extend the field system in VitalGrid.
Fields control how a cell:
- Shows a value
- Edits a value
- Maps UI values back into the stored row data
VitalGrid ships with built-in fields and lets you add your own.
For concepts and overall architecture, see:
- Getting Started
- Concepts Guide
- Architecture docs for internal details
1. What is a field?
A field is a small unit that tells VitalGrid:
- How to render a cell for a given column type
- How to handle editing
- How to translate between:
- Stored value in the row
- Display value in the UI
Each column points at a field type by its type key.
Examples:
type: "text"type: "number"type: "date"type: "status"type: "boolean"type: "tag"- Your own types:
type: "priority",type: "avatar", etc.
2. Built-in fields
VitalGrid includes common field types in src/components/fields/:
- Text
- Number
- Date
- Boolean
- Status
- Tag
- Avatar (depending on your version)
Each built-in field has a dedicated reference section with its props:
- See the per-field pages under "Fields" in the navigation.
- Each page uses
<AutoTypeTable />wired to the actual TypeScript definitions.
Each built-in field:
- Knows how to render its value
- Handles editing with sensible UI
- Uses simple value shapes (string, number, boolean, etc.)
You can map them in your factory:
typeonVitalGridColumnmust match a key in yourfieldsconfig.
Example:
- Column:
{ id: "name", header: "Name", type: "text" } - Factory fields:
{ text: createTextField() }
3. Where fields fit in the data flow
Keep this simple mental model:
- Backend and adapters:
- Convert raw backend data into your row object
- Implement
VitalGridDataSource
- VitalGrid:
- Uses
VitalGridColumnto describe columns - Uses TanStack Table for rows/cells
- Uses
- Fields:
- Run at cell render time
- Work with values from the row
- Do not know about HTTP, Convex, or other backend details
Important:
- Fields handle UI-level transformation.
- Backend-specific mapping belongs in your data source, not in fields.
4. Field responsibilities
A field definition (conceptually) can include:
- A React component to render and edit the cell
- Optional helpers:
toDisplayValue(value)toPersistenceValue(value)
Use these helpers to keep logic clear.
Typical patterns:
-
Text:
- Stored:
string - Display: same string
- Stored:
-
Number:
- Stored:
number | null - Display: string in an
<input>but converted back to number on save
- Stored:
-
Date:
- Stored: ISO string
- Display: localized string or
Datein the picker
-
Status:
- Stored: status key, for example
"todo" | "in-progress" | "done" - Display: a label and color badge
- Stored: status key, for example
-
Tag / multi-select:
- Stored: array of keys
- Display: chips or labels
Guideline:
toDisplayValue:- Take the stored value and prepare what the component needs.
toPersistenceValue:- Take UI input and convert back to the stored value.
These are about formatting and shape, not about API calls.
5. Registering fields in the factory
You register fields when you create your VitalGrid factory.
Example:
import {
createVitalGrid,
// assuming you export these from your library entrypoint
createTextField,
createNumberField,
createDateField,
createStatusField,
} from "@/vital-grid.js";
export const VitalGrid = createVitalGrid({
features: {
sorting: true,
filtering: true,
columnResizing: true,
virtualization: true,
},
fields: {
text: createTextField(),
number: createNumberField(),
date: createDateField(),
status: createStatusField(),
// you can add custom fields here too
},
});Then your columns can use these keys:
type: "text"type: "number"type: "date"type: "status"
VitalGrid will:
- Look up
fields[column.type] - Use that field's component and helpers to render/edit cells
6. Creating a simple custom field
Here is a simple pattern for a custom field.
Example: a priority field that shows colored labels.
Conceptual shape:
import React from "react";
import type { FieldDefinition } from "@/lib/field.js";
const PRIORITY_LABELS: Record<string, string> = {
low: "Low",
medium: "Medium",
high: "High",
};
const PRIORITY_COLOR: Record<string, string> = {
low: "#10B981",
medium: "#F59E0B",
high: "#EF4444",
};
export function createPriorityField(): FieldDefinition<string> {
return {
// Optional: config object if you need settings
config: {},
// Map stored value -> display value (here they are similar)
toDisplayValue: (value) => value ?? "medium",
// Map UI value -> stored value
toPersistenceValue: (value) => value ?? "medium",
// Render component
Component: function PriorityCell(props) {
const value = props.value ?? "medium";
const label = PRIORITY_LABELS[value] ?? value;
const color = PRIORITY_COLOR[value] ?? "#6B7280";
return (
<span
style={{
display: "inline-flex",
alignItems: "center",
padding: "2px 8px",
borderRadius: 999,
fontSize: 12,
backgroundColor: `${color}20`,
color,
}}
>
{label}
</span>
);
},
};
}Use it in the factory:
import { createVitalGrid } from "@/vital-grid.js";
import { createPriorityField } from "./priority-field";
export const VitalGrid = createVitalGrid({
features: {
sorting: true,
columnResizing: true,
},
fields: {
priority: createPriorityField(),
},
});And in column definitions:
type: "priority"
7. Where to NOT put logic
To keep things clean:
-
Do NOT:
- Call backend APIs from your field components.
- Encode backend specific payloads in
toDisplayValueortoPersistenceValue. - Mix adapter logic inside fields.
-
DO:
- Use fields to format and control UI for a single cell.
- Keep backend mapping in your
VitalGridDataSourceor converter functions.
Examples of correct separation:
- Convex rows:
- Adapter flattens Convex
{ _id, values: { [columnId]: any } }into row objects your grid uses. - Fields just work with the flattened values.
- Adapter flattens Convex
- REST API:
- Adapter knows how to call
/rowsand/columns. - Fields only know about the values inside the row object.
- Adapter knows how to call
8. Tips for advanced usage
Some patterns that work well:
-
Shared config via
settings:- Store per-column settings in
VitalGridColumn.settings. - Read them inside your field component.
- Example: options for a status field, max length for text, etc.
- Store per-column settings in
-
Read-only vs editable:
- Your field component can decide:
- When to show plain text
- When to show an input or dropdown
- For example:
- Single click to edit
- Double click to edit
- Or always editable
- Your field component can decide:
-
Validation:
- Do UI validation in the field component.
- For backend validation, let the data source or backend handle it.
- You can show errors in the UI based on responses, but keep the call logic out of the field itself.
-
Reuse:
- Use factories to share field sets across projects.
- For example:
- A "core" set of fields for most grids.
- Extra fields for specific products.
9. Checklist
When you add or use fields, check:
- Column
typematches a key in your factoryfieldsconfig. - Field component does not hard-code backend logic.
-
toDisplayValue/toPersistenceValueonly handle shape/format changes. - Any complex or backend-specific mapping is done in:
- Your
VitalGridDataSourceimplementation, or - Small converter utilities.
- Your
If you follow these rules, your field system stays simple:
- Easy to read
- Easy to extend
- Safe across different backends