VitalGrid
Guides

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:


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:

  • type on VitalGridColumn must match a key in your fields config.

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 VitalGridColumn to describe columns
    • Uses TanStack Table for rows/cells
  • 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
  • Number:

    • Stored: number | null
    • Display: string in an <input> but converted back to number on save
  • Date:

    • Stored: ISO string
    • Display: localized string or Date in the picker
  • Status:

    • Stored: status key, for example "todo" | "in-progress" | "done"
    • Display: a label and color badge
  • 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 toDisplayValue or toPersistenceValue.
    • Mix adapter logic inside fields.
  • DO:

    • Use fields to format and control UI for a single cell.
    • Keep backend mapping in your VitalGridDataSource or 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.
  • REST API:
    • Adapter knows how to call /rows and /columns.
    • Fields only know about the values inside the row object.

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.
  • 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
  • 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 type matches a key in your factory fields config.
  • Field component does not hard-code backend logic.
  • toDisplayValue / toPersistenceValue only handle shape/format changes.
  • Any complex or backend-specific mapping is done in:
    • Your VitalGridDataSource implementation, or
    • Small converter utilities.

If you follow these rules, your field system stays simple:

  • Easy to read
  • Easy to extend
  • Safe across different backends