ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Handsontable React 自定义编辑器实战:用 EditorComponent 构建 Emoji 反馈单元格

Handsontable React 自定义编辑器实战:用 EditorComponent 构建 Emoji 反馈单元格 Handsontable React 自定义编辑器实战用 EditorComponent 构建 Emoji 反馈单元格【免费下载链接】handsontableJavaScript Data Grid / Data Table with a Spreadsheet Look Feel. Works with React, Angular, and Vue. Supported by the Handsontable team ⚡项目地址: https://gitcode.com/gh_mirrors/ha/handsontable本指南讲解如何在 Handsontable 的 React 封装handsontable/react-wrapper中利用EditorComponent构建一个基于 emoji 按钮、、的反馈单元格编辑器点击即选、方向键 / Tab 键循环切换、每列可配置不同选项。读完本文你将掌握 React 组件式编辑器component-based editor的完整开发套路——render prop 渲染、onPrepare读取单元格配置、shortcuts快捷键注册并能在此基础上扩展出星级评分、文本标签、自定义渲染器等任意选择型编辑器。Overview在数据网格产品中反馈 / 评分类列非常常见用户需要从一组固定候选中快速选择一个值。Handsontable 自带编辑器以文本输入为主而通过 React 封装提供的EditorComponent你可以把任意 React UI 变成单元格编辑器——编辑器打开时在单元格上方渲染按钮组用户点击或按键选择后立即写入单元格。本文以功能路线图feature-roadmap表格为例表格含一个 Feedback 列// 三选一和一个 Rating 列1–5 星级两列复用同一个FeedbackEditor组件仅通过HotColumn的config属性传入不同选项数组。难度初级Beginner耗时约 15 分钟额外依赖库无仅 React 与handsontable/react-wrapper本身What Youll Build一个具备以下能力的单元格编辑器编辑时在单元格位置显示一排 emoji 反馈按钮、、浏览时单元格显示当前选中的 emoji 值支持键盘导航ArrowLeft/ArrowRight/Tab循环切换选项点击按钮即选中并关闭编辑器通过 ReactEditorComponent的 render prop 渲染 UI样式由外部 CSS如feedback-editor类控制在onPrepare中读取cellProperties.config实现每列独立选项配置Prerequisites安装 React 封装包npm install handsontable/react-wrapper需要准备的环境React 16.8需要 hooks 支持handsontable/react-wrapper包基本 React 知识hooks、JSXStep 1: 导入依赖import { useState, useEffect, useCallback, ComponentProps } from react; import { HotTable, HotColumn, EditorComponent } from handsontable/react-wrapper; import { registerAllModules } from handsontable/registry; registerAllModules();导入内容说明EditorComponent—— 用于创建自定义编辑器的 React 组件底层通过useHotEditorhook 与 Handsontable 原生编辑器生命周期桥接HotTable与HotColumn—— React 封装的表格与列组件useState/useEffect/useCallback—— 管理编辑器内部状态与快捷键registerAllModules()—— 注册 Handsontable 全部功能模块从源码结构看EditorComponent定义在 wrappers/react-wrapper/src/hotEditor.tsx它通过useHotEditorT()获得value、setValue、finishEditing、isOpen、row、col等编辑器 API详见 types.tsx 中的UseHotEditorImplT接口渲染结果通过 React Portal 挂载到表格容器之上createEditorPortal见 helpers.tsx。Step 2: 创建编辑器组件创建一个使用EditorComponent render prop 模式的 React 组件type EditorComponentProps ComponentPropstypeof EditorComponentstring; const FeedbackEditor () { const [config, setConfig] useStatestring[]([, , ]); return ( EditorComponentstring {({ value, setValue, finishEditing }) ( div classNameeditor {config.map((item) ( button key{item} className{button ${value item ? active : }} onClick{() { setValue(item); finishEditing(); }} {item} /button ))} /div )} /EditorComponent ); };这段代码发生了什么EditorComponent包裹你的编辑器 UIchildren是一个接收编辑器状态的函数render prop 模式value—— 当前编辑器值setValue—— 更新值的函数finishEditing—— 保存并关闭编辑器为config中每个选项渲染一个按钮根据当前值高亮激活按钮.active类关键概念Render prop 模式EditorComponent以函数作为 children把编辑器状态作为参数传入状态管理value和setValue由EditorComponent提供React 组件使用标准 React 模式JSX、className、onClick源码级原理解读EditorComponent渲染的是一个绝对定位的容器divzIndex: 100默认display: none仅在编辑器打开onOpen时设为block见 hotEditor.tsx。value的初值来自onOpen时读取的原生编辑器实例当前值setEditorValue(hotCustomEditorInstanceRef.current?.getValue())——这正是编辑时高亮当前选中项的数据来源。而setValue除了更新 React 状态还会同步写入底层原生编辑器实例hotCustomEditorInstanceRef.current?.setValue(newValue)保证数据链路一致。Step 3: 添加样式用 CSS 或内联样式美化编辑器容器和按钮const FeedbackEditor () { const [config, setConfig] useStatestring[]([, , ]); return ( EditorComponentstring {({ value, setValue, finishEditing }) ( style{ .editor { box-sizing: border-box; display: flex; gap: 3px; padding: 3px; background: rgb(238, 238, 238); border: 1px solid rgb(204, 204, 204); border-radius: 4px; height: 100%; width: 100%; } .button.active { background: #007bff; color: white; } .button:hover { background: #f0f0f0; } .button { background: #fff; color: black; border: none; padding: 0; margin: 0; height: 100%; width: 100%; font-size: 16px; font-weight: bold; text-align: center; cursor: pointer; } }/style div classNameeditor {config.map((item, _index, _array) ( button key{item} className{button ${value item ? active : }} onClick{() { setValue(item); finishEditing(); }} style{{ width: ${100 / _array.length}% }} {item} /button ))} /div / )} /EditorComponent ); };这段代码发生了什么容器使用 flexbox 实现按钮横向排列按钮宽度根据config长度动态计算激活按钮为蓝色背景hover 效果提升交互体验关键样式点display: flex—— 按钮横向布局gap: 3px—— 按钮间距width: ${100 / _array.length}%—— 按钮宽度随选项数量自动均分_array即当前config数组.active类 —— 高亮当前选中按钮样式作用域说明这里用style标签做 CSS-in-JS样式会注入全局。若在多个编辑器实例或与其他页面样式混用时出现冲突推荐改用 Step 7 的外部 CSS 文件方案或给类名加前缀。EditorComponent打开的容器默认带position: absolute与白色背景实际渲染尺寸由源码中的applyEditorPosition依据目标单元格的矩形getBoundingClientRect计算并同步含 RTL 布局处理见 hotEditor.tsx因此容器设置height: 100%; width: 100%即可完整覆盖单元格。Step 4: 通过 onPrepare 读取列配置不同列可能需要不同的选项集合。用onPrepare在编辑器打开前读取列级配置const FeedbackEditor () { const [config, setConfig] useStatestring[]([, , ]); const onPrepare: EditorComponentProps[onPrepare] ( _row, _column, _prop, _TD, _originalValue, cellProperties ) { // Read config from column definition if (cellProperties.config) { setConfig(cellProperties.config as string[]); } }; return ( EditorComponentstring onPrepare{onPrepare} {({ value, setValue, finishEditing }) ( // ... editor UI )} /EditorComponent ); };这段代码发生了什么onPrepare在编辑器打开前被调用对应 Handsontable 原生编辑器的prepare生命周期cellProperties包含列级配置从cellProperties.config读取自定义的选项数组更新组件 state 以反映当前列专属的选项为什么这很重要不同列可以有不同的选项集合一个编辑器组件多种配置基于列设置实现动态选项源码级原理解读React 封装通过MethodsMap将原生编辑器生命周期映射为 hooks 回调——prepare → onPrepare、open → onOpen、close → onClose、focus → onFocus见 hotEditor.tsx。makeEditorClass会生成一个继承Handsontable.editors.BaseEditor的CustomEditor类把BaseEditor.prototype上的方法逐一包装先调用父类实现再触发对应的 hooks 回调见 hotEditor.tsx。onPrepare的第六个参数正是 Handsontable 的CellProperties——它继承了GridSettings的索引签名因此config这类自定义属性可以合法地直接挂在列配置上types.tsx 中的类型注释明确说明这类未在GridSettings上声明的选项仍保持可赋值。Step 5: 添加键盘快捷键通过shortcuts属性注册键盘导航const FeedbackEditor () { const [config, setConfig] useStatestring[]([, , ]); const [shortcuts, setShortcuts] useStateEditorComponentProps[shortcuts]([]); const getNextValue useCallback((value: string) { const index config.indexOf(value); return index config.length - 1 ? config[0] : config[index 1]; }, [config]); const getPrevValue useCallback((value: string) { const index config.indexOf(value); return index 0 ? config[config.length - 1] : config[index - 1]; }, [config]); useEffect(() { setShortcuts([ { keys: [[ArrowRight], [Tab]], callback: ({ value, setValue }, _event) { setValue(getNextValue(value)); return false; // Prevent default Tab behavior } }, { keys: [[ArrowLeft]], callback: ({ value, setValue }, _event) { setValue(getPrevValue(value)); } } ]); }, [config, getNextValue, getPrevValue]); return ( EditorComponentstring shortcuts{shortcuts} {({ value, setValue, finishEditing }) ( // ... editor UI )} /EditorComponent ); };这段代码发生了什么ArrowRight / Tab切换到下一个选项到达末尾后回绕到第一个ArrowLeft切换到上一个选项到达开头后回绕到最后一个callback的第一个参数接收{ value, setValue, finishEditing }返回false阻止默认行为例如 Tab 默认会跳到下一个单元格键盘导航的收益无需鼠标即可快速选择对纯键盘用户友好直观的左右方向导航Tab 在选项间循环而非切换单元格源码级原理解读EditorComponent在onOpen时通过hot.getShortcutManager().setActiveContextName(editor)激活编辑上下文并把shortcuts注册进shortcutManager.getContext(editor)每条快捷键默认归属group: custom-editor且position默认为before、relativeToGroup默认为editorManager.handlingEditor——即在编辑器管理器默认处理之前拦截按键见 hotEditor.tsx。回调被包装为接收{ value: currentValue.current, setValue, finishEditing }的函数其中value取自useRef实时同步的最新值useEffect中currentValue.current value保证每次按键回调都能拿到当前选项。编辑器关闭时onClose会通过removeShortcutsByGroup(shortcutsGroup)卸载这些快捷键避免泄漏到网格的其他操作中。Step 6: 完整编辑器组件把前面各步骤整合在一起type EditorComponentProps ComponentPropstypeof EditorComponentstring; const FeedbackEditor () { const [config, setConfig] useStatestring[]([, , ]); const [shortcuts, setShortcuts] useStateEditorComponentProps[shortcuts]([]); const onPrepare: EditorComponentProps[onPrepare] ( _row, _column, _prop, _TD, _originalValue, cellProperties ) { if (cellProperties.config) { setConfig(cellProperties.config as string[]); } }; const getNextValue useCallback((value: string) { const index config.indexOf(value); return index config.length - 1 ? config[0] : config[index 1]; }, [config]); const getPrevValue useCallback((value: string) { const index config.indexOf(value); return index 0 ? config[config.length - 1] : config[index - 1]; }, [config]); useEffect(() { setShortcuts([ { keys: [[ArrowRight], [Tab]], callback: ({ value, setValue }, _event) { setValue(getNextValue(value)); return false; } }, { keys: [[ArrowLeft]], callback: ({ value, setValue }, _event) { setValue(getPrevValue(value)); } } ]); }, [config, getNextValue, getPrevValue]); return ( EditorComponentstring onPrepare{onPrepare} shortcuts{shortcuts} {({ value, setValue, finishEditing }) ( style{ .editor { box-sizing: border-box; display: flex; gap: 3px; padding: 3px; background: rgb(238, 238, 238); border: 1px solid rgb(204, 204, 204); border-radius: 4px; height: 100%; width: 100%; } .button.active:hover, .button.active { background: #007bff; color: white; } .button:hover { background: #f0f0f0; } .button { background: #fff; color: black; border: none; padding: 0; margin: 0; height: 100%; width: 100%; font-size: 16px; font-weight: bold; text-align: center; cursor: pointer; } }/style div classNameeditor {config.map((item, _index, _array) ( button key{item} className{button ${value item ? active : }} onClick{() { setValue(item); finishEditing(); }} style{{ width: ${100 / _array.length}% }} {item} /button ))} /div / )} /EditorComponent ); };这段代码发生了什么状态管理config和shortcuts由 React hooks 管理onPrepare读取列级配置shortcuts键盘导航处理器Render prop根据 config 渲染按钮样式CSS-in-JS 控制编辑器外观Step 7: 在 HotTable 中使用把编辑器组件挂到HotTable/HotColumn上const ExampleComponent () { return ( HotTable autoRowSize{true} rowHeaders{true} autoWrapRow{true} licenseKeynon-commercial-and-evaluation heightauto data{data} colHeaders{true} HotColumn width{250} editor{FeedbackEditor} config{[, , ]} datafeedback titleFeedback / HotColumn width{250} editor{FeedbackEditor} config{[1, 2, 3, 4, 5]} datastars titleRating (1-5) / /HotTable ); };这段代码发生了什么editor{FeedbackEditor}—— 将编辑器组件绑定到该列config{[, , ]}—— 该列专属的选项集合同一个编辑器组件在不同列使用不同配置关键特性编辑器组件可复用支持按列配置TypeScript 类型安全源码级原理解读HotColumn在挂载/更新时通过isComponentEditor(props.editor)判断editor是否为 React 组件非 boolean若是则调用makeEditorClass(...)生成一个 Handsontable 原生编辑器类写入列设置同时用EditorContextProvider把hooks 引用 原生编辑器实例引用下发给编辑器组件见 hotColumn.tsx。渲染层面createEditorPortal用ReactDOM.createPortal把FeedbackEditor /挂载到列专属的 portal host 中——这解释了为什么FeedbackEditor内部不需要自己useRef拿 DOMEditorComponent已处理容器与定位你的组件只管渲染按钮 UI。此外列级config属性与data、title一样都会经SettingsMapper.getSettings进入该列的ColumnSettingsHotColumn会过滤_columnIndex、_getOwnerDocument、children等内部属性后合并其余 props最终在onPrepare的cellProperties中可读。How It Works —— 完整流程初始渲染单元格显示当前 emoji 值、 或 用户双击或按 Enter编辑器打开onPrepare读取列配置编辑器打开EditorComponent把容器绝对定位并覆盖在单元格上方applyEditorPosition依据单元格getBoundingClientRect计算 top/left/width/height滚动时通过afterScrollHorizontally/afterScrollVerticallyhooks 自动刷新位置见 hotEditor.tsx按钮展示所有选项可见当前值高亮用户交互点击按钮 → 调用setValue(item)与finishEditing()按 ArrowLeft/Right → 快捷键回调更新 value按 Tab → 在选项间循环阻止默认的单元格跳转视觉反馈选中按钮以蓝色高亮用户确认按 Enter、点击按钮或点击其他区域保存值写入单元格编辑器关闭单元格显示所选 emoji其中保存并关闭的finishEditing对应源码中useHotEditor返回的finishEditing()它直接调用原生编辑器实例的finishEditing()方法见 hotEditor.tsx从而走完 Handsontable 的afterChange等标准数据链路。Enhancements1. 自定义渲染器美化展示EditorComponent只负责编辑态浏览态的外观由渲染器renderer决定。加一个自定义渲染器让 emoji 居中放大import { rendererFactory } from handsontable/renderers; const cellDefinition { renderer: rendererFactory(({ td, value }) { td.innerHTML div styletext-align: center; font-size: 1.5em; padding: 4px; ${value || } /div ; }) }; // Use in HotColumn HotColumn editor{FeedbackEditor} renderer{cellDefinition.renderer} config{[, , ]} datafeedback /说明居中显示 emoji增大字号提升可读性添加内边距留出间距2. 更多反馈选项增加 emoji 选项数量HotColumn editor{FeedbackEditor} config{[, , , ❤️, , ⭐]} datafeedback /编辑器会根据 config 长度自动调整按钮宽度width: ${100 / _array.length}%。3. 自定义按钮样式增强按钮外观style{ .button { padding: 8px; border: 2px solid #ddd; background: white; color: #333; border-radius: 4px; cursor: pointer; font-size: 1.2em; transition: all 0.2s; } .button.active { border-color: #007bff; background: #007bff; color: white; } .button:hover { transform: scale(1.05); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } }/style4. 基于单元格属性的动态配置onPrepare已经处理了动态配置——只需给不同列传入不同configHotColumn editor{FeedbackEditor} config{[, , ❤️, ]} datafeedback /5. 悬浮提示Tooltip给按钮加提示文本{config.map((item) { const tooltips: Recordstring, string { : Positive feedback, : Negative feedback, : Neutral feedback }; return ( button key{item} className{button ${value item ? active : }} onClick{() { setValue(item); finishEditing(); }} title{tooltips[item] || } {item} /button ); })}6. 用文本标签代替 emoji编辑器对任意字符串值都有效不止 emojiHotColumn editor{FeedbackEditor} config{[Positive, Negative, Neutral]} datafeedback /7. 使用外部 CSS 文件把样式抽取到独立 CSS 文件/* feedback-editor.css */ .editor { box-sizing: border-box; display: flex; gap: 3px; padding: 3px; background: rgb(238, 238, 238); border: 1px solid rgb(204, 204, 204); border-radius: 4px; height: 100%; width: 100%; } .button.active { background: #007bff; color: white; } .button:hover { background: #f0f0f0; } .button { background: #fff; color: black; border: none; padding: 0; margin: 0; height: 100%; width: 100%; font-size: 16px; font-weight: bold; text-align: center; cursor: pointer; }import ./feedback-editor.css; const FeedbackEditor () { // ... component code without style tag };AccessibilityReact 的button元素本身具备可访问性但可以进一步强化{config.map((item, index) ( button key{item} className{button ${value item ? active : }} onClick{() { setValue(item); finishEditing(); }} aria-label{${item} feedback option} aria-pressed{value item} tabIndex{value item ? 0 : -1} {item} /button ))}键盘导航Tab进入编辑器聚焦当前激活按钮Arrow Left/Right在选项间循环通过 shortcutsEnter选中当前选项并结束编辑Escape取消编辑Click直接选择ARIA 属性aria-label描述每个按钮的含义aria-pressed标识选中状态tabIndex控制键盘焦点顺序让焦点落在当前选中项其余按钮保持-1可被方向键切换Performance Considerations为什么这套方案很快React 虚拟 DOM只在 value 变化时高效更新无外部库除 React 本身外零额外开销高效重渲染仅当 config 或 value 变化时才重新渲染原生事件使用浏览器优化的 click 处理器React Hooks 优化useCallback与useEffect保证快捷键只在 config 变化时重建const getNextValue useCallback((value: string) { const index config.indexOf(value); return index config.length - 1 ? config[0] : config[index 1]; }, [config]); // Only recreate if config changes useEffect(() { setShortcuts([...]); }, [config, getNextValue, getPrevValue]); // Only update when dependencies change从源码看EditorComponent内部同样注意了性能细节定位容器默认display: none编辑器关闭时不参与渲染与布局滚动位置刷新仅注册在afterScrollHorizontally/afterScrollVertically两个 hook 上关闭时即注销unRegisterScrollHooks避免常驻监听拖累网格滚动性能。TypeScript SupportEditorComponent是完整类型化的可以指定值类型EditorComponentstring {({ value, setValue, finishEditing }) { // TypeScript knows value is string | undefined // TypeScript knows setValue accepts string return ( // ... editor UI ); }} /EditorComponent数值型反馈EditorComponentnumber {({ value, setValue, finishEditing }) { // TypeScript knows value is number | undefined return ( // ... editor UI ); }} /EditorComponent配套的 props 类型如ComponentPropstypeof EditorComponentstring可直接从react包导出用于声明onPrepare、shortcuts等回调的类型享受完整的自动补全与编译期检查。Best Practices用onPrepare做按单元格/按列配置—— 访问cellProperties读取自定义选项正确处理键盘事件—— 用shortcuts做导航需要阻止默认行为如 Tab 跳格时在回调中返回false恰当地调用finishEditing()—— 在用户确认变更时调用Enter、失焦、按钮点击保持 render prop 函数精简—— 复杂逻辑抽到独立组件或 hooks 中辅助函数用useCallback包裹—— 避免不必要的重渲染在useEffect中更新 shortcuts—— 保证快捷键始终与当前 config 一致依赖数组包含config、getNextValue、getPrevValueWhat you learned你用 Handsontable 的EditorComponent构建了一个 React emoji 反馈单元格编辑器使用 render prop 模式渲染可配置的选项按钮通过onPrepare从cellProperties读取按列配置并借助shortcuts属性实现键盘导航。这个模式可以推广到任何从固定候选集合中选值的场景——星级评分、状态选择、优先级标记等。Next stepsFeedback (JavaScript) —— 同一模式的非 React 实现使用editorFactory与 Handsontable CSS tokensFeedback Editor (Angular) —— Angular 版本使用HotCellEditorAdvancedComponentStar Rating (React) —— 另一个使用EditorComponent做数值选择的 React 编辑器示例如需了解 React 封装中EditorComponent、HotColumn与原生编辑器生命周期的桥接细节可直接阅读仓库源码hotEditor.tsx、hotColumn.tsx、helpers.tsx、types.tsx。【免费下载链接】handsontableJavaScript Data Grid / Data Table with a Spreadsheet Look Feel. Works with React, Angular, and Vue. Supported by the Handsontable team ⚡项目地址: https://gitcode.com/gh_mirrors/ha/handsontable创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表