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:
'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 "Итого" и 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>
)
}'use client'
import { FieldTableEditor } from '@letar/forms-shadcn'
import { DemoForm, DemoPageLayout } from '../_components'
export default function TableEditorDemoPage() {
return (
<DemoPageLayout
title="FieldTableEditor (beta)"
description={'Не `createField()`-поле, компонует `form.Field(mode="array")` напрямую — изолированная песочница со '
+ 'своим array-полем `items`. `sortable` — native HTML5 drag&drop (без @dnd-kit, beta-упрощение).'}
>
<DemoForm<{ items: { product: string; qty: number; price: number }[] }>
defaultValues={{
items: [
{ product: 'Клавиатура', qty: 1, price: 5990 },
{ product: 'Мышь', qty: 2, price: 1490 },
],
}}
onSubmit={(value) => {
// eslint-disable-next-line no-console
console.log('table submit', value)
}}
>
<FieldTableEditor
name="items"
label="Позиции заказа"
sortable
selectable
columns={[
{ name: 'product', label: 'Товар', width: '50%' },
{ name: 'qty', label: 'Кол-во', width: '15%', align: 'right' },
{ name: 'price', label: 'Цена', width: '15%', align: 'right' },
{
name: 'total',
label: 'Итого',
width: '20%',
align: 'right',
computed: (row) => (Number(row.qty) || 0) * (Number(row.price) || 0),
format: (v) => `${Number(v).toLocaleString('ru-RU')} ₽`,
},
]}
addLabel="Добавить позицию"
footer={[{
column: 'total',
aggregate: 'sum',
label: 'Итого:',
format: (v) => `${v.toLocaleString('ru-RU')} ₽`,
}]}
/>
<button
type="submit"
className="bg-primary text-primary-foreground mt-4 rounded-md px-4 py-2 text-sm font-medium"
>
Отправить
</button>
</DemoForm>
</DemoPageLayout>
)
}import { AppForm } from '@letar/forms-vue'
import { defineComponent, h, ref } from 'vue'
import { z } from 'zod'
import { FieldTableEditor } from '../../src/index'
/**
* Изолированный пример FieldTableEditor — самодостаточный файл (своя Zod-схема, свой AppForm).
* Те же данные и колонки, что в React-примере (`table-editor-demo/page.tsx`), для честного
* сравнения при переключении оси Framework (React ↔ Vue) в form-docs.
*/
const schema = z.object({
items: z.array(
z.object({
product: z.string(),
qty: z.number(),
price: z.number(),
}),
),
})
export const TableEditorDemo = defineComponent({
name: 'TableEditorDemo',
setup() {
const submitted = ref<Record<string, unknown> | null>(null)
return () =>
h('div', { class: 'space-y-6' }, [
h(
AppForm,
{
schema,
initialValue: {
items: [
{ product: 'Молоко', qty: 2, price: 89 },
{ product: 'Хлеб', qty: 1, price: 45 },
],
},
onSubmit: (value: Record<string, unknown>) => {
submitted.value = value
},
},
{
default: () => [
h(FieldTableEditor, {
name: 'items',
columns: [
{ name: 'product', label: 'Товар', width: '40%' },
{ name: 'qty', label: 'Кол-во', width: '20%', align: 'right' },
{ name: 'price', label: 'Цена', width: '20%', align: 'right' },
],
addLabel: 'Добавить позицию',
}),
h(
'button',
{ type: 'submit', class: 'bg-primary text-primary-foreground rounded-md px-4 py-2 text-sm' },
'Сохранить',
),
],
},
),
submitted.value
? h('pre', { class: 'bg-muted mt-4 rounded-md p-3 text-xs' }, JSON.stringify(submitted.value, null, 2))
: null,
])
},
})Angular-пример для этого поля появится позже.
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)}`,
},
]}
/>Footer Aggregates
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
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | required | Array field name |
label | string | — | Table label |
columns | TableColumnDef[] | auto from schema | Column definitions |
addLabel | string | "Добавить строку" | Add button text |
sortable | boolean | false | Enable drag&drop row sorting |
selectable | boolean | false | Enable checkbox row selection |
footer | TableFooterDef[] | — | Footer aggregate definitions |
maxRows | number | from schema | Maximum rows |
minRows | number | from schema | Minimum rows |
clipboard | boolean | true | Enable paste from Excel |
emptyText | string | "Нет данных..." | Empty state text |
size | 'sm' | 'md' | 'lg' | 'sm' | Table size |
striped | boolean | false | Striped rows |
disabled | boolean | false | Disable editing |
readOnly | boolean | false | Read-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
| Key | Action |
|---|---|
| Click | Start editing cell |
| Tab | Next editable cell |
| Shift+Tab | Previous editable cell |
| Enter | Confirm and move to next row |
| Escape | Cancel editing |
| Arrow Up/Down | Move between rows |
Live Example
Try the interactive example on forms-example.letar.best.