本文将探讨如何将 wangEditor 改造成受控组件,并且可以与 Antd 的 Form 表单连用。其他编辑器应该类似,本文抛个砖引个玉,希望给大家带来启发。
前言
最近接到一个小需求,要将业务中的 textarea 文本框改为富文本编辑器,调研了几种编辑器后,最终选择了 wangEditor 来实现需求。由于原来的 textarea 是放在 antd 的 form 表单中使用的,因而编辑器需要改造成受控组件的形式。
受控与非受控组件
下面先用两个示例,感受一下受控组件和非受控组件的区别:
非受控组件
<input />
受控组件
const [value, setValue] = useState('')
<input value={value} onChange={(e) => setValue(e.detail.value)}/>
简单来讲,受控组件可以由外部状态影响到输入框的输入值。
组件封装
回到编辑器,他本身就类似于上面的非受控组件,但相比 input, 他没有 value 和 onChange 来改变编辑器的内容。幸运的是,他提供了 editor.txt.html()
方法可以 获取 和 设置 编辑器的内容,同时也提供了 editor.config.onchange
钩子函数来监听用户输入。
于是很容易封装出这样的代码
<!------------Editor.tsx---------------->
import { useEffect, useRef } from 'react'
import E from 'wangeditor'
interface Props {
value: string
onChange(v: string): void
}
export default ({ value, onChange }: Props) => {
const editorEle = useRef(null)
const editorRef = useRef<E | null>(null)
useEffect(() => {
const editor = new E(editorEle.current)
editorRef.current = editor
editor.config.onchange = onChange
editor.create()
}, [])
useEffect(() => {
editorRef.current?.txt.html(value)
}, [value])
return <div ref={editorEle}></div>
}
问题引出
使用 useEffect 监听 value 值,调用 editor.txt.html()
方法来更新编辑器的输入框内容。想法没什么问题,是实际使用,你会发现各种各样的问题,比如: 如何保留光标位置 ,性能较差等等。
分析问题
要解决这个问题,首先考虑一下,什么情况下 value 会变化?
如果 value 值只由 onChange 引起变化,那其实编辑器中输入框的值永远是与 value 值是相等的,即 value === editor.txt.html()
, 因而没有必要再去手动设置一遍 editor.txt.html(value)
, 所以上面代码中,监听 value 值这一段代码可以去掉了。
但情况往往没有这么简单,如果初始时,编辑器里的值是依赖接口返回的,则还是需要去监听 value 值的变化,组件里面,没有办法区分你数据是怎么来的,需不需要进行 editor.txt.html(value)
的操作。
<!------------App.tsx---------------->
import { useEffect, useState } from 'react'
import Editor from './Editor'
function App() {
const [value, setValue] = useState('')
useEffect(() => {
// 编辑器初始值依赖接口返回
fetch('http://localhost:3000').then(res => res.text()).then(setValue)
}, [])
return (
<div className="App">
<Editor value={value} onChange={setValue} />
</div>
)
}
解决方案
上面也提到了,如果 value 值是由 onChange 引起的,那么 value
永远等于 editor.txt.html()
, 如果不等于,一定就是由外部因素引起 value 的变化,由外部引起时,再调用 editor.txt.html(value)
就好了。
export default ({ value, onChange }: Props) => {
// ...省略部分代码
useEffect(() => {
if (value !== editorRef.current?.txt.html()) {
editorRef.current?.txt.html(value)
}
}, [value])
return <div ref={editorEle}></div>
}
可能有人会疑惑,这种方法还是没解决保留光标位置的问题啊。
这种方法确实没解决这个问题,但是还请考虑,什么情况下,需要除去 onChange 方法外,去改变 value 值呢? 初始化数据?reset 表单?这些是不是没必要保留光标位置呢?
代码示例
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
import E from 'wangeditor'
interface Props {
defaultValue?: string
value: string
onChange(v: string): void
}
export default forwardRef(({ value, onChange, defaultValue }: Props, ref) => {
const editorEleRef = useRef(null)
const editorRef = useRef<E | null>(null)
useEffect(() => {
const editor = new E(editorEleRef.current)
editorRef.current = editor
editor.config.onchange = onChange
if(defaultValue) editor.txt.html(defaultValue)
editor.create()
return () => {
editor.destroy()
}
}, [])
useEffect(() => {
if (value !== editorRef.current?.txt.html()) {
editorRef.current?.txt.html(value)
}
}, [value])
useImperativeHandle(ref, () => ({
value: editorRef.current?.txt.html() || ''
}))
return <div ref={editorEleRef}></div>
})