React.js实现ContentEditable中用户输入文本的正则转HTML功能
React实现文本变量格式化与预定义变量功能
原代码核心问题分析
你的代码没能达到预期效果主要有3个原因:
- 丢失结构信息:用
textContent获取内容会直接忽略已渲染的<span>标签,只保留纯文本,导致每次编辑都会把之前的变量格式清零。 - 光标混乱:每次用
dangerouslySetInnerHTML重新渲染内容,会让光标跳转到输入框开头,严重影响编辑体验。 - 缺乏预定义变量支持:没有提供便捷的变量插入入口,用户只能手动输入格式。
优雅实现方案
下面是一套兼顾编辑体验、状态安全和功能需求的实现方案:
1. 核心思路
- 用状态存储原始文本内容(包含
($变量名)格式的文本),而非渲染后的HTML,便于直接发送到后端。 - 渲染时将原始文本解析为普通文本+变量节点(
<span class="variable">),避免使用dangerouslySetInnerHTML。 - 监听编辑事件,将DOM内容还原为原始文本格式,同步更新状态。
- 实现预定义变量一键插入功能,提升用户体验。
2. 完整代码实现
import { useState, useRef } from 'react'; // 预定义变量列表 const PREDEFINED_VARS = ['username', 'orderId', 'totalAmount', 'shippingAddress']; // 解析原始文本为JSX节点 const parseContent = (content) => { // 正则匹配($变量名)格式 const regex = /\(\$(.*?)\)/g; const parts = content.split(regex); const result = []; for (let i = 0; i < parts.length; i++) { if (i % 2 === 0) { // 普通文本节点 parts[i] && result.push(parts[i]); } else { // 变量节点,添加样式类 result.push( <span key={i} className="variable"> ${parts[i]} </span> ); } } return result; }; function CreateTemplate() { const [rawContent, setRawContent] = useState(''); const editableRef = useRef(null); // 处理编辑事件,将DOM内容还原为原始文本格式 const handleInput = () => { const element = editableRef.current; if (!element) return; let text = ''; const nodes = element.childNodes; for (const node of nodes) { if (node.nodeType === Node.TEXT_NODE) { // 普通文本直接追加 text += node.textContent; } else if (node.nodeName === 'SPAN' && node.classList.contains('variable')) { // 变量节点还原为($变量名)格式 const varName = node.textContent.replace(/^\$/, ''); text += `($${varName})`; } else { // 其他未知节点,直接取文本内容 text += node.textContent; } } setRawContent(text); }; // 插入预定义变量到光标位置 const insertVariable = (varName) => { const element = editableRef.current; if (!element) return; const selection = window.getSelection(); if (selection.rangeCount === 0) { // 无选中内容,直接追加到末尾 setRawContent(prev => prev + `($${varName})`); return; } const range = selection.getRangeAt(0); // 删除选中内容 range.deleteContents(); // 插入变量文本 const textNode = document.createTextNode(`($${varName})`); range.insertNode(textNode); // 移动光标到变量后方 range.setStartAfter(textNode); range.setEndAfter(textNode); selection.removeAllRanges(); selection.addRange(range); // 手动触发input事件更新状态 element.dispatchEvent(new Event('input')); }; // 发送到后端的逻辑,直接使用rawContent即可 const handleSubmit = () => { console.log('发送到后端的内容:', rawContent); // fetch('/api/submit', { method: 'POST', body: JSON.stringify({ content: rawContent }) }); }; return ( <div className="template-editor"> {/* 预定义变量按钮组 */} <div className="var-buttons"> {PREDEFINED_VARS.map(varName => ( <button key={varName} onClick={() => insertVariable(varName)} className="var-button" > ${varName} </button> ))} </div> {/* 可编辑区域 */} <div ref={editableRef} contentEditable onInput={handleInput} className="editable-area" > {parseContent(rawContent)} </div> {/* 提交按钮 */} <button onClick={handleSubmit} className="submit-btn"> 提交模板 </button> {/* 样式 */} <style jsx>{` .template-editor { max-width: 800px; margin: 20px auto; padding: 0 20px; } .var-buttons { margin-bottom: 10px; display: flex; gap: 8px; flex-wrap: wrap; } .var-button { padding: 4px 12px; background: #e5f3ff; border: 1px solid #2196f3; border-radius: 4px; cursor: pointer; color: #1976d2; } .editable-area { min-height: 150px; padding: 12px; border: 1px solid #ddd; border-radius: 4px; outline: none; } .variable { background: #fff3cd; padding: 2px 6px; border-radius: 3px; border: 1px solid #ffeeba; font-family: monospace; cursor: default; } .submit-btn { margin-top: 10px; padding: 8px 16px; background: #2196f3; color: white; border: none; border-radius: 4px; cursor: pointer; } `}</style> </div> ); } export default CreateTemplate;
3. 关键细节说明
- 状态管理:
rawContent存储用户输入的原始文本,包含($变量名)格式,直接用于后端提交,无需额外转换。 - 内容解析:
parseContent函数将原始文本拆分为普通文本和变量节点,用React的JSX安全渲染,避免XSS风险。 - 编辑同步:
handleInput遍历DOM节点,将变量节点还原为($变量名)格式,确保状态与编辑内容一致。 - 变量插入:
insertVariable利用window.getSelection定位光标,精准插入变量文本,同时更新光标位置,保持编辑连贯性。
4. 样式优化
给变量节点添加背景色、边框等样式,让用户清晰区分普通文本和变量,提升视觉体验。
内容的提问来源于stack exchange,提问作者Fares Galal
相关产品推荐
相关产品推荐

