@letar/forms

TableEditor

Inline editable table for array fields — tabular data entry with navigation, computed columns, and clipboard support

Overview

Form.Field.TableEditor is an inline editable table for array fields. It's a tabular alternative to Form.Group.List — instead of card-based layout, data is presented in a compact table with click-to-edit cells.

Best for:

  • Order items (10-50 rows)
  • Inventory operations (100+ positions)
  • Data import from Excel
  • Bulk record creation (HR, CRM)

Basic Usage

<Form
  initialValue={{
    items: [
      { product: 'Milk', qty: 2, price: 89 },
      { product: 'Bread', qty: 1, price: 45 },
    ],
  }}
  onSubmit={handleSubmit}
>
  <Form.Field.TableEditor
    name="items"
    columns={[
      { name: 'product', label: 'Product', width: '40%' },
      { name: 'qty', label: 'Qty', width: '20%', align: 'right' },
      { name: 'price', label: 'Price', width: '20%', align: 'right' },
    ]}
    addLabel="Add item"
  />
  <Form.Button.Submit>Save</Form.Button.Submit>
</Form>

Click any cell to start editing. Press Tab to move to next cell, Enter to move to next row, Escape to cancel editing.

Full example

Read directly from the form-develop-app / form-develop-app-shadcn sandbox source at build time:

apps/form-develop-app/src/app/table-editor-demo/page.tsx
'use client'

import { Box, Heading, Text, VStack } from '@chakra-ui/react'
import { formatRubles } from '@letar/format-utils'
import { Form } from '@letar/forms'
import { useState } from 'react'
import { DemoPageLayout } from '../_components'

export default function TableEditorDemoPage() {
  const [submittedData, setSubmittedData] = useState<Record<string, unknown> | null>(null)

  return (
    <DemoPageLayout
      title="Form.Field.TableEditor"
      description="Инлайн-редактируемая таблица для array-полей — замена карточного FormGroupList"
    >
      <VStack gap={8} align="stretch">
        {/* Пример 1: Заказ с товарами */}
        <Box>
          <Heading size="md" mb={3}>
            Заказ с товарами
          </Heading>
          <Text fontSize="sm" color="fg.muted" mb={4}>
            Кастомные колонки, computed &quot;Итого&quot; и footer с суммой. Кликните по ячейке для редактирования. Tab
            — следующая ячейка, Enter — следующая строка.
          </Text>
          <Form
            debug
            initialValue={{
              customer: '',
              items: [
                { product: 'Молоко', qty: 2, price: 89 },
                { product: 'Хлеб', qty: 1, price: 45 },
                { product: 'Сыр', qty: 1, price: 320 },
              ],
            }}
            onSubmit={(data) => setSubmittedData(data as Record<string, unknown>)}
          >
            <Form.Field.String name="customer" label="Покупатель" />
            <Form.Field.TableEditor
              name="items"
              label="Товары"
              columns={[
                { name: 'product', label: 'Товар', width: '40%' },
                { name: 'qty', label: 'Кол-во', width: '15%', align: 'right' },
                { name: 'price', label: 'Цена', width: '15%', align: 'right' },
                {
                  name: 'total',
                  label: 'Итого',
                  width: '15%',
                  align: 'right',
                  computed: (row) => (Number(row.qty) || 0) * (Number(row.price) || 0),
                  format: (v) => formatRubles(Number(v)),
                },
              ]}
              addLabel="Добавить товар"
              footer={[
                { column: 'total', aggregate: 'sum', label: 'Итого:', format: (v) => formatRubles(v) },
              ]}
              selectable
              helperText="Можно вставлять данные из Excel (Ctrl+V)"
            />
            <Form.Button.Submit>Оформить заказ</Form.Button.Submit>
          </Form>
        </Box>

        {/* Пример 2: Простая таблица */}
        <Box>
          <Heading size="md" mb={3}>
            Простая таблица контактов
          </Heading>
          <Text fontSize="sm" color="fg.muted" mb={4}>
            Минимальный пример — колонки определяются вручную.
          </Text>
          <Form
            debug
            initialValue={{
              contacts: [{ name: 'Иван Петров', email: 'ivan@example.com', phone: '+7 900 123-45-67' }],
            }}
            onSubmit={(data) => setSubmittedData(data as Record<string, unknown>)}
          >
            <Form.Field.TableEditor
              name="contacts"
              label="Контакты"
              columns={[
                { name: 'name', label: 'Имя' },
                { name: 'email', label: 'Email' },
                { name: 'phone', label: 'Телефон' },
              ]}
              addLabel="Добавить контакт"
              size="md"
              sortable
            />
            <Form.Button.Submit>Сохранить</Form.Button.Submit>
          </Form>
        </Box>

        {/* Результат */}
        {submittedData && (
          <Box p={4} bg="bg.subtle" borderRadius="md">
            <Heading size="sm" mb={2}>
              Отправленные данные:
            </Heading>
            <pre style={{ fontSize: '12px', overflow: 'auto' }}>{JSON.stringify(submittedData, null, 2)}</pre>
          </Box>
        )}
      </VStack>
    </DemoPageLayout>
  )
}

Computed Columns

Add read-only computed columns that auto-calculate from row data:

<Form.Field.TableEditor
  name="items"
  columns={[
    { name: 'product', width: '40%' },
    { name: 'qty', width: '15%', align: 'right' },
    { name: 'price', width: '15%', align: 'right' },
    {
      name: 'total',
      label: 'Total',
      align: 'right',
      computed: (row) => Number(row.qty) * Number(row.price),
      format: (v) => `$${Number(v).toFixed(2)}`,
    },
  ]}
/>

Add SUM, AVG, COUNT, MIN, or MAX calculations in the footer:

<Form.Field.TableEditor
  name="items"
  columns={[...]}
  footer={[
    { column: 'total', aggregate: 'sum', label: 'Grand Total:' },
    { column: 'qty', aggregate: 'count', label: 'Items:' },
  ]}
/>

Row Selection

Enable checkbox selection for bulk operations:

<Form.Field.TableEditor
  name="items"
  columns={[...]}
  selectable={true}
/>

Selected rows can be deleted in bulk via the toolbar.

Clipboard (Copy/Paste from Excel)

By default, clipboard={true}. Users can paste tab-separated data from Excel or Google Sheets. The data is parsed and rows are added automatically, with values coerced to the correct types.

<Form.Field.TableEditor
  name="items"
  columns={[...]}
  clipboard={true}
/>

Props

PropTypeDefaultDescription
namestringrequiredArray field name
labelstringTable label
columnsTableColumnDef[]auto from schemaColumn definitions
addLabelstring"Добавить строку"Add button text
sortablebooleanfalseEnable drag&drop row sorting
selectablebooleanfalseEnable checkbox row selection
footerTableFooterDef[]Footer aggregate definitions
maxRowsnumberfrom schemaMaximum rows
minRowsnumberfrom schemaMinimum rows
clipboardbooleantrueEnable paste from Excel
emptyTextstring"Нет данных..."Empty state text
size'sm' | 'md' | 'lg''sm'Table size
stripedbooleanfalseStriped rows
disabledbooleanfalseDisable editing
readOnlybooleanfalseRead-only mode

Column Definition

interface TableColumnDef {
  name: string // Field name in row object
  label?: string // Column header
  width?: string // CSS width ('40%', '200px')
  align?: 'left' | 'center' | 'right'
  computed?: (row) => unknown // Computed value function
  format?: (value) => string // Display format
  hidden?: boolean // Hide column
  readOnly?: boolean // Disable cell editing
}

Keyboard Navigation

KeyAction
ClickStart editing cell
TabNext editable cell
Shift+TabPrevious editable cell
EnterConfirm and move to next row
EscapeCancel editing
Arrow Up/DownMove between rows

Live Example

Try the interactive example on forms-example.letar.best.

On this page