CellEditor
cell-editor在表格单元格里就地编辑,静止时看起来和纯文本一样
用法
基础用法
静止时就是表格里的一段文字,聚焦才出浅底 + 下划线。失焦或 Enter 提交,Esc 回滚到进入编辑前的值;值没变不会调 onCommit。
提交记录
还没有提交。点进去看一眼再点走不会发请求,只有值真的变了才发。
const [value, setValue] = useState("杭州云枢科技有限公司");
<CellEditor
value={value}
placeholder="未填写"
onCommit={(next) => setValue(next)}
/>核对表 · missing 与多行
核对场景的主场:空字段用 missing 降成灰斜体,一眼看出哪儿还没填;长文本用 multiline,高度靠 CSS field-sizing 跟着内容长,不是 JS 测高。
| 字段 | 值 |
|---|---|
| 客户名称 | |
| 联系人 | |
| 联系电话 | |
| 开票地址 | |
| 备注 |
提交记录
还没有提交。点进去看一眼再点走不会发请求,只有值真的变了才发。
<CellEditor
value={field.value}
multiline={field.multiline}
missing={field.value.trim() === ""}
placeholder="未填写"
onCommit={(next) => save(field.key, next)}
/>异步提交
onCommit 返回 Promise 时这一格自己进 pending 态并禁用,消费方不必再传一个 saving 标志位。
改一个字再失焦:请求飞出去的那 0.9 秒里这一格自己禁用。
<CellEditor
value={value}
onCommit={async (next) => {
await api.patch(id, { phone: next });
setValue(next);
}}
/>何时用
核对、补录已有数据的表格:用户扫一遍,看到不对的就地改一个字。这类表里每个单元格常驻一个「长得像文本、其实是输入框」的编辑器,没有编辑/保存按钮,失焦即提交该格。
和 EditableTable 不是同一个东西的两种皮肤,是两种交互契约:
| EditableTable(行级) | CellEditor(逐格) | |
|---|---|---|
| 进入编辑 | 点「编辑」/ 新增行 | 不需要进入,永远可编辑 |
| 提交粒度 | 整行一次 | 单格 |
| 提交时机 | 点保存 | 失焦 / Enter |
| 撤销 | 取消整行 | Esc 回滚该格 |
| 视觉 | 明确的表单控件 | 无边框透明底,静止时和纯文本无异 |
| 典型场景 | 报价单、账单明细录入 | 核对 / 补录已有数据 |
本组件只做编辑器这一层,表格外壳交给 Table(用 align-top + 不截断换行),这样排序 / 冻结列 / 虚拟滚动不必在编辑器里重造一遍。要「点编辑 → 改 → 保存整行」用 EditableTable;要一个普通的带边框输入框用 Input / Textarea。
导入
import { CellEditor } from "@hulianui/ui"Props
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| value* | string | - | 已提交值(受控数据源)。提交成功后把新值写回这里,组件据此重置内部草稿与判等基准 |
| validate | (next: string) => string | undefined | - | 提交前校验:返回字符串=错误消息,拦住 onCommit 并把该串显示在格子下方;返回 undefined(或空串)放行。见下 |
| missing | boolean | false | 「这个字段还没填」:降成 muted + italic,让「空」和「填了空格」一眼可分 |
| multiline | boolean | false | 多行档(textarea + CSS field-sizing: content 自增高);默认单行 input。要按运行时变量切换多行,请分支渲染两个 CellEditor,见「禁忌 / 坑」 |
| revertOnError | boolean | false | onCommit 的 Promise reject 时把草稿一并退回上一次提交值。判等基准无论开关都会退,见下 |
| blurOnCommit | boolean | false | Enter 提交后让出焦点。校验被拦下时不让出(错误就在这一格,得让用户接着改) |
| blurOnEscape | boolean | false | Esc 回滚后让出焦点 |
| variant | "default" | "cell" | "cell" | 外观档,透传给内层 Input / Textarea。"cell" 无边框透明底;同一行里其余列是普通输入框时用 "default",见下 |
| size | "xs" | "sm" | "md" | "lg" | "md" | 字号档,透传给内层 Input / Textarea |
| disabled | boolean | false | 禁用。onCommit 返回 Promise 时组件在 pending 期间自己追加禁用,不必再传 |
| placeholder | string | - | 占位文案(核对表里一般写「未填写」) |
| className | string | - | 落在最外层节点上 |
其余属性按档透传到编辑控件本身:单行档收 <input> 的原生属性(name / type / maxLength / autoComplete …),多行档收 <textarea> 的(name / rows / wrap …)。rows 在多行档里是「最少几行」的下限,cell 档下默认 1 行。
原生 size 传不进来:它是 <input> 的字符宽度,与上表的档位 size 同名不同义,两者只能留一个。要按字符数定宽用 CSS 宽度。
Events
| 事件 | 类型 | 说明 |
|---|---|---|
| onCommit | (next: string) => void | Promise<void> | blur 与 Enter 触发。值与上次提交相同时不会调用;返回 Promise 则 pending 期间自身禁用。传了 validate 且它返回错误串时不会调用 |
| onDraftChange | (draft: string) => void | 草稿每次变化时的只读回声(每敲一个键一次),判等 / 校验 / pending 的既有语义全不受影响。给「随打字变化的派生 UI」用:已填计数、实时预览、每键落 localStorage。只反映键入 —— Esc 回滚与外部写回 value 不会广播 |
顺序是「判等 → validate → onCommit」:值没变根本不校验(那是上一次已经放行过的),校验没过则值不出去。
值真正出去仍然只看 onCommit。如果你在 onDraftChange 里落库,就把失焦即提交这条契约绕过去了 —— 判等与 validate 都拦不住它。
禁忌 / 坑
- 别在 `onCommit` 里自己判等:组件已经判过了,值没变根本不会调进来。核对场景下用户会大量「点进去看一眼再点走」,判等这一层就是为了不让一整屏空提交打到后端。
- Esc 之后的 blur 不会重发旧值:Esc 把草稿写回上一次提交值,紧随其后的 blur 判等直接短路。消费方不需要再维护「刚按过 Esc」的标志位。Esc 默认只回滚不移走焦点,要它连焦点一起让出就开
blurOnEscape,别自己补blur()。 - `multiline` 要写成字面量:属性集按
multiline分叉(单行档收<input>的原生属性,多行档收<textarea>的),传一个 boolean 变量时 TS 认不出走哪一档。如果多行与否真的由运行时决定,分支渲染两个CellEditor。 - `onDraftChange` 不是提交口:它每敲一个键响一次,判等与
validate都不参与。在里面落库等于把失焦即提交这条契约整个绕过去。 - Enter 提交,Shift+Enter 换行:多行档里换行让给 Shift+Enter;单行档里 Enter 被
preventDefault,不会误提交所在的 form。 - 自增高是 CSS 的 `field-sizing: content`,不是 JS 测高:表格里几十个格同时读
scrollHeight会在滚动时明显掉帧,而且和列宽变化互相触发。别再往外面套一层测高逻辑。 - 能在客户端判的非法值走 `validate`,别放到 `onCommit` 里再回滚:回滚发生时光标已经在下一格,用户只会看到自己改的东西自己变回去了。
onCommit里剩下的是只有服务端才知道的失败(重名、并发冲突),那类仍然要自己 catch。 - `validate` 返回空串等于放行:一条看不见的错误却拦着提交,比不校验更糟 —— 用户只看到这格存不进去,屏幕上什么都没有。想拦就给一句能读的话。
- `onCommit` 失败时别把异常吞掉:组件靠 Promise reject 才知道这次没存进去(据此退回判等基准,让用户能重试)。如果你在
catch里 toast 完就不再抛出,组件看到的是一次成功的提交。报错文案与是否回滚草稿(revertOnError)仍然由消费方决定,组件不替你选。 - 父级必须把新值写回 `value`:不写回时组件仍以自己的草稿显示,但下一次外部刷新会把界面拉回旧值。
- 放进 [Table](/zh/components/table) 时 `columns` 必须 memo:cell 函数经 TanStack 的
flexRender当组件类型渲染,identity 一变整格卸载重挂 —— 挂了onBlur提交的编辑器会被重挂时的 blur 误触发。
相关
EditableTable · Table · Input · Textarea · ProTable
Playground
<CellEditor
value={value}
placeholder="未填写"
onCommit={(next) => setValue(next)}
/>