本文将探讨如何将 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>
})