Built-in editors for cell editing mode.
Cell editors are specialized input components used during inline editing. The ActiveGrid provides pre-built editors for common data types with automatic keyboard handling and validation support.
import { TextEditor, NumberEditor, SelectEditor } from '@workspace/active-grid';Text input for string values.
import { TextEditor } from '@workspace/active-grid';
<TextEditor
value="Hello"
onChange={(newValue) => setValue(newValue)}
onBlur={handleSave}
onKeyDown={handleKeyDown}
autoFocus
/>string - Current value(value: string) => void - Change handler() => void - Blur handler (save changes)(e: KeyboardEvent) => void - Keyboard handlerboolean - Auto-focus on mountstring - Additional CSS classes{
accessorKey: 'name',
header: 'Name',
meta: {
editable: true,
editType: 'text',
},
}Numeric input with validation.
import { NumberEditor } from '@workspace/active-grid';
<NumberEditor
value={123}
onChange={(newValue) => setValue(newValue)}
onBlur={handleSave}
min={0}
max={1000}
step={1}
autoFocus
/>number - Current value(value: number) => void - Change handler() => void - Blur handler(e: KeyboardEvent) => void - Keyboard handlernumber - Minimum valuenumber - Maximum valuenumber - Step increment (default: 1)boolean - Auto-focus on mountstring - Additional CSS classesDropdown selection from options.
string - Current selected value(value: string) => void - Change handlerArray<{label: string; value: string}> - Available optionsboolean - Auto-focus on mountstring - Additional CSS classesDate input with picker.
The date editor automatically uses the appropriate input type for the platform.
Checkbox for boolean values.
Create custom editors for specialized input types:
All editors accept className prop:
All editors include:
{
accessorKey: 'quantity',
header: 'Quantity',
meta: {
editable: true,
editType: 'number',
validate: (value) => {
if (value < 0) return 'Must be positive';
if (value > 1000) return 'Maximum is 1000';
return undefined;
},
},
}import { SelectEditor } from '@workspace/active-grid';
<SelectEditor
value="option1"
onChange={(newValue) => setValue(newValue)}
options={[
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
]}
autoFocus
/>{
accessorKey: 'status',
header: 'Status',
meta: {
editable: true,
editType: 'select',
editOptions: [
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
{ label: 'Pending', value: 'pending' },
],
},
}{
accessorKey: 'birthDate',
header: 'Birth Date',
meta: {
editable: true,
editType: 'date',
},
}{
accessorKey: 'isActive',
header: 'Active',
meta: {
editable: true,
editType: 'boolean',
},
}import { useState, useEffect, useRef } from 'react';
function TagEditor({ value, onChange, onBlur, autoFocus }) {
const [tags, setTags] = useState(value || []);
const [input, setInput] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (autoFocus && inputRef.current) {
inputRef.current.focus();
}
}, [autoFocus]);
const handleAddTag = () => {
if (input.trim()) {
const newTags = [...tags, input.trim()];
setTags(newTags);
onChange(newTags);
setInput('');
}
};
const handleRemoveTag = (index: number) => {
const newTags = tags.filter((_, i) => i !== index);
setTags(newTags);
onChange(newTags);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddTag();
} else if (e.key === 'Escape') {
onBlur();
}
};
return (
<div className="flex flex-wrap gap-1 p-1">
{tags.map((tag, index) => (
<Badge key={index}>
{tag}
<button onClick={() => handleRemoveTag(index)}>×</button>
</Badge>
))}
<input
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={onBlur}
placeholder="Add tag..."
className="flex-1 outline-none"
/>
</div>
);
}
// Use in column
{
accessorKey: 'tags',
header: 'Tags',
cell: ({ getValue, row, column, table }) => {
const [editing, setEditing] = useState(false);
const value = getValue() as string[];
if (editing) {
return (
<TagEditor
value={value}
onChange={(newValue) => {
// Update data
}}
onBlur={() => setEditing(false)}
autoFocus
/>
);
}
return (
<div onClick={() => setEditing(true)}>
{value.map(tag => <Badge key={tag}>{tag}</Badge>)}
</div>
);
},
}function ValidatedNumberEditor({ value, onChange, onBlur, validate }) {
const [localValue, setLocalValue] = useState(value);
const [error, setError] = useState<string>();
const handleChange = (newValue: number) => {
setLocalValue(newValue);
const validationError = validate?.(newValue);
setError(validationError);
if (!validationError) {
onChange(newValue);
}
};
return (
<div>
<NumberEditor
value={localValue}
onChange={handleChange}
onBlur={onBlur}
className={error ? 'border-red-500' : ''}
autoFocus
/>
{error && (
<div className="text-xs text-red-500 mt-1">{error}</div>
)}
</div>
);
}function DebouncedTextEditor({ value, onChange, onBlur, debounceMs = 300 }) {
const [localValue, setLocalValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
onChange(localValue);
}, debounceMs);
return () => clearTimeout(timer);
}, [localValue, onChange, debounceMs]);
return (
<TextEditor
value={localValue}
onChange={setLocalValue}
onBlur={onBlur}
autoFocus
/>
);
}function AsyncValidatedEditor({ value, onChange, onBlur, asyncValidate }) {
const [localValue, setLocalValue] = useState(value);
const [error, setError] = useState<string>();
const [validating, setValidating] = useState(false);
useEffect(() => {
if (localValue === value) return;
const validate = async () => {
setValidating(true);
const error = await asyncValidate(localValue);
setError(error);
setValidating(false);
if (!error) {
onChange(localValue);
}
};
const timer = setTimeout(validate, 500);
return () => clearTimeout(timer);
}, [localValue, value, onChange, asyncValidate]);
return (
<div className="relative">
<TextEditor
value={localValue}
onChange={setLocalValue}
onBlur={onBlur}
className={error ? 'border-red-500' : ''}
autoFocus
/>
{validating && (
<Spinner className="absolute right-2 top-2" size="sm" />
)}
{error && (
<div className="text-xs text-red-500 mt-1">{error}</div>
)}
</div>
);
}const columns: GridColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
meta: {
editable: true,
editType: 'text',
validate: (value) => {
if (!value) return 'Name is required';
if (value.length < 2) return 'Minimum 2 characters';
return undefined;
},
},
},
{
accessorKey: 'age',
header: 'Age',
meta: {
editable: true,
editType: 'number',
validate: (value) => {
if (value < 18) return 'Must be 18 or older';
if (value > 120) return 'Invalid age';
return undefined;
},
},
},
{
accessorKey: 'role',
header: 'Role',
meta: {
editable: true,
editType: 'select',
editOptions: [
{ label: 'Admin', value: 'admin' },
{ label: 'User', value: 'user' },
{ label: 'Guest', value: 'guest' },
],
},
},
{
accessorKey: 'isActive',
header: 'Active',
meta: {
editable: true,
editType: 'boolean',
},
},
];const columns: GridColumnDef<Product>[] = [
{
accessorKey: 'name',
header: 'Product',
meta: {
editable: true,
editType: 'text',
},
},
{
accessorKey: 'price',
header: 'Price',
meta: {
editable: true,
editType: 'number',
valueFormatter: (value) => `$${value.toFixed(2)}`,
validate: (value) => {
if (value <= 0) return 'Price must be positive';
return undefined;
},
},
},
{
accessorKey: 'category',
header: 'Category',
meta: {
editable: true,
editType: 'select',
editOptions: [
{ label: 'Electronics', value: 'electronics' },
{ label: 'Clothing', value: 'clothing' },
{ label: 'Books', value: 'books' },
],
},
},
{
accessorKey: 'launchDate',
header: 'Launch Date',
meta: {
editable: true,
editType: 'date',
},
},
];<TextEditor
value={value}
onChange={onChange}
className="font-mono text-sm"
/>
<NumberEditor
value={value}
onChange={onChange}
className="text-right"
/>
<SelectEditor
value={value}
onChange={onChange}
options={options}
className="w-full"
/>import { TextEditor, NumberEditor, SelectEditor } from '@workspace/active-grid';
type EditHandler = (value: any) => void;
<TextEditor
value="text"
onChange={(value: string) => console.log(value)}
/>
<NumberEditor
value={123}
onChange={(value: number) => console.log(value)}
/>
<SelectEditor
value="option"
onChange={(value: string) => console.log(value)}
options={[
{ label: 'Option', value: 'option' },
]}
/>