Input and selection components
Configuration and change events for input, checkbox, radio, select, and toggle in cells or headers. Cell input/select appear only for a single selected row. Omitting callbacks does not update application data.
Commit timing and data updates
Section titled “Commit timing and data updates”Input typing changes an internal draft. Enter or blur calls onChange then onValueChange when the value differs from the last commit. Blur after Enter does not commit the same value twice. Both callbacks run if provided, so update data in just one of them. props.onChange is not the commit callback. Do not assume built-in Escape cancellation.
select/radio emit a string value even when option.value was a number or boolean. Convert to the business type explicitly. checkbox/toggle emit a boolean checked value. Their immediate visual state is temporary; persist the final value in parent data.
Try it
Section titled “Try it”Select a row and edit its name. Parent data stays unchanged while typing; Enter commits once. Role selection, checkbox, toggle, and button use dedicated callbacks without incrementing ordinary cell/row click counters.
import { useMemo, useRef, useState } from 'react';import { CominsTable, type CominsTableColumn, type CominsTableRef, type CominsVirtualListItem } from 'comins-table';import 'comins-table/styles.css';
type Row = { id: string; name: string; role: string; active: boolean; ready: boolean; progress: number; items: CominsVirtualListItem[] };const initialRows: Row[] = ['alpha', 'beta'].map((id, index) => ({ id, name: index ? 'Beta' : 'Alpha', role: 'reader', active: false, ready: false, progress: index ? 75 : 40, items: Array.from({ length: 12 }, (_, i) => ({ value: `${id}-${i + 1}`, label: `Task ${i + 1}`, searchText: `task-${i + 1}` })),}));
export default function TableComponentsDemo({ lang = 'ko', mode = 'inputs' }: { lang?: string; mode?: 'inputs' | 'list' }) { const ko = lang === 'ko'; const table = useRef<CominsTableRef<Row>>(null); const [rows, setRows] = useState(initialRows); const [events, setEvents] = useState<string[]>([]); const [clicks, setClicks] = useState({ cell: 0, row: 0 }); const record = (message: string) => setEvents(previous => [...previous.slice(-11), message]); const update = (id: string, patch: Partial<Row>) => setRows(previous => previous.map(row => row.id === id ? { ...row, ...patch } : row)); const columns = useMemo<CominsTableColumn<Row>[]>(() => { const identity: CominsTableColumn<Row> = { field: 'id', label: 'ID', width: 130, header: { components: [{ type: 'menu', id: 'actions', direction: 'right', props: { children: '⋯', 'aria-label': ko ? '헤더 메뉴' : 'Header menu' }, items: [{ type: 'label', label: ko ? '동작' : 'Actions' }, { value: 'inspect', label: ko ? '상태 확인' : 'Inspect state' }, { type: 'divider' }, { value: 'disabled', label: ko ? '비활성 항목' : 'Disabled item', disabled: true }], onOpenChange: ({ open }) => record(`menu:${open}`), onSelect: ({ value }) => record(`menu-select:${value}`), }] }, }; if (mode === 'list') return [identity, { field: 'items', label: ko ? '작업 목록' : 'Tasks', width: 420, cell: { components: [{ type: 'virtual-list', id: 'tasks', items: ({ row }) => row.data.items, props: { limit: 3, itemHeight: 28, height: 140, more: true, searchable: true, 'aria-label': ko ? '작업 목록' : 'Tasks' }, searchFilter: ({ item, value }) => (item.searchText ?? '').includes(value.trim().toLowerCase()), onClickItem: ({ row, itemIndex, value }) => record(`item:${row.id}:${itemIndex}:${value}`), }] }, }]; return [identity, { field: 'name', label: ko ? '이름' : 'Name', width: 180, cell: { components: [{ type: 'input', id: 'name', props: ({ value, row }) => ({ value: String(value), 'aria-label': `${row.id} name` }), onChange: ({ row, value }) => { update(String(row.id), { name: value }); record(`commit:${row.id}:${value}`); }, onValueChange: ({ row, value }) => record(`value:${row.id}:${value}`), }] } }, { field: 'role', label: ko ? '역할' : 'Role', width: 140, cell: { components: [{ type: 'select', id: 'role', options: [{ value: 'reader', label: ko ? '읽기' : 'Reader' }, { value: 'editor', label: ko ? '편집' : 'Editor' }], props: ({ value, row }) => ({ value: String(value), 'aria-label': `${row.id} role` }), onValueChange: ({ row, value }) => { update(String(row.id), { role: value }); record(`role:${value}`); }, }] } }, { field: 'active', label: ko ? '활성' : 'Active', width: 90, cell: { components: [{ type: 'checkbox', id: 'active', props: ({ value, row }) => ({ checked: Boolean(value), 'aria-label': `${row.id} active` }), onCheckedChange: ({ row, checked }) => { update(String(row.id), { active: checked }); record(`active:${checked}`); }, }] } }, { field: 'role', id: 'role-radio', label: ko ? '역할 라디오' : 'Role radio', width: 190, cell: { components: [{ type: 'radio', id: 'role-radio', options: [{ value: 'reader', label: ko ? '읽기' : 'Reader' }, { value: 'editor', label: ko ? '편집' : 'Editor' }], props: ({ value, row }) => ({ value: String(value), 'aria-label': `${row.id} role radio` }), onValueChange: ({ row, value }) => { update(String(row.id), { role: value }); record(`radio:${value}`); }, }] } }, { field: 'ready', label: ko ? '준비' : 'Ready', width: 90, cell: { components: [{ type: 'toggle', id: 'ready', props: ({ value, row }) => ({ checked: Boolean(value), children: value ? 'ON' : 'OFF', 'aria-label': `${row.id} ready` }), onCheckedChange: ({ row, checked }) => { update(String(row.id), { ready: checked }); record(`ready:${checked}`); }, }] } }, { field: 'progress', label: ko ? '진행률' : 'Progress', width: 120, cell: { components: [{ type: 'progress', id: 'progress', props: ({ value }) => ({ value: Number(value), max: 100 }) }] } }, { field: 'id', id: 'action', label: ko ? '동작' : 'Action', width: 110, cell: { components: [{ type: 'button', id: 'inspect', props: ({ row }) => ({ children: ko ? '확인' : 'Inspect', 'aria-label': `${row.id} inspect` }), onClick: ({ row }) => record(`button:${row.id}`), }] } }, ]; }, [ko, mode]); return <div className="demo" data-demo={`table-components-${mode}`}> <div className="demo-toolbar"> <button onClick={() => table.current?.setSelectedRows([0])}>{ko ? '첫 행 선택' : 'Select first row'}</button> <button onClick={() => { setRows(initialRows); setEvents([]); setClicks({ cell: 0, row: 0 }); table.current?.setSelectedRows([]); }}>{ko ? '초기화' : 'Reset'}</button> </div> <div className="table-viewport" style={mode === 'list' ? { height: 380 } : undefined}> <CominsTable ref={table} columns={columns} data={rows} getRowId={row => row.id} onChangeData={setRows} getRowHeight={mode === 'list' ? () => 'auto' : undefined} onClickCell={() => setClicks(value => ({ ...value, cell: value.cell + 1 }))} onClickRow={() => setClicks(value => ({ ...value, row: value.row + 1 }))} /> </div> <p className="demo-status" data-component-clicks>{ko ? '일반 클릭' : 'Ordinary clicks'}: cell {clicks.cell} / row {clicks.row}</p> <details open><summary>{ko ? '이벤트 순서' : 'Event order'}</summary><pre data-component-events>{JSON.stringify(events, null, 2)}</pre></details> <details><summary>{ko ? '부모 데이터' : 'Parent data'}</summary><pre data-component-data>{JSON.stringify(rows.map(({ items, ...row }) => ({ ...row, itemCount: items.length })), null, 2)}</pre></details> </div>;}CominsInputComponentConfig
Section titled “CominsInputComponentConfig”type: "input";Required literal input.
props?: CominsTableComponentProps<TPayload, React.InputHTMLAttributes<HTMLInputElement>>;Input HTML attributes or resolver. Defaults to an empty string without value/defaultValue. Even numeric inputs emit strings.
onValueChange
Section titled “onValueChange”onValueChange?: (payload: TPayload & { value: string; }) => void;Emits the committed string. No callback by default; follows the commit timing above.
onChange
Section titled “onChange”onChange?: (payload: TPayload & { event: CominsInputCommitEvent; value: string; }) => void;Emits the commit event and string value before onValueChange. See CominsInputCommitEvent below.
CominsCheckboxComponentConfig
Section titled “CominsCheckboxComponentConfig”type: "checkbox";Required literal checkbox.
props?: CominsTableComponentProps<TPayload, React.InputHTMLAttributes<HTMLInputElement>>;checked/defaultChecked defaults to false. Set input attributes such as disabled and aria-label.
onCheckedChange
Section titled “onCheckedChange”onCheckedChange?: (payload: TPayload & { checked: boolean; }) => void;Emits the new checked value. Omission leaves parent data unchanged.
CominsRadioComponentConfig
Section titled “CominsRadioComponentConfig”type: "radio";Required literal radio.
props?: CominsTableComponentProps<TPayload, React.HTMLAttributes<HTMLDivElement> & { value?: CominsComponentPrimitiveValue; }>;Div HTML attributes and selected value. In cells, omitted value falls back to the raw cell value. Disable items through options[].disabled.
onValueChange
Section titled “onValueChange”onValueChange?: (payload: TPayload & { value: string; }) => void;Emits the selected option.value as a string.
options
Section titled “options”options: CominsTableOptions<TPayload>;Required option array or payload resolver. Provide label/value and keep values unique after string conversion.
CominsSelectComponentConfig
Section titled “CominsSelectComponentConfig”type: "select";Required literal select.
props?: CominsTableComponentProps<TPayload, React.SelectHTMLAttributes<HTMLSelectElement>>;Select HTML attributes or resolver. Bind the parent value through props.value.
onValueChange
Section titled “onValueChange”onValueChange?: (payload: TPayload & { value: string; }) => void;Emits a string on selection change. No callback by default.
options
Section titled “options”options: CominsTableOptions<TPayload>;Required option array or payload resolver. Provide label/value and keep values unique after string conversion.
CominsToggleComponentConfig
Section titled “CominsToggleComponentConfig”type: "toggle";Required literal toggle.
props?: CominsTableComponentProps<TPayload, React.ButtonHTMLAttributes<HTMLButtonElement> & { checked?: boolean; }>;Button HTML attributes plus checked, initially false. Supply button content with children.
onCheckedChange
Section titled “onCheckedChange”onCheckedChange?: (payload: TPayload & { checked: boolean; }) => void;Emits the toggled checked boolean. No callback by default.
CominsTableComponentOption
Section titled “CominsTableComponentOption”label: React.ReactNode;Required displayed option content.
value: CominsComponentPrimitiveValue;Required string/number/boolean value, emitted as a string by select/radio.
disabled
Section titled “disabled”disabled?: boolean;Default false. Disables the option.
CominsInputCommitEvent
Section titled “CominsInputCommitEvent”export type CominsInputCommitEvent = React.ChangeEvent<HTMLInputElement> | React.FocusEvent<HTMLInputElement> | React.KeyboardEvent<HTMLInputElement>;The published union includes Change/Focus/Keyboard events. The current input implementation commits on Enter/blur with Keyboard/Focus events.