React组件:内容可编辑的div,可以使用危险地设置SetInnerHTML跳转插入符号的位置

时间:2019-04-23 13:38:02

标签: javascript reactjs react-hooks

我目前正在使用React构建带有contenteditable div的组件。由于内容是由用户生成的,因此我试图通过使用state使组件dangerouslySetInnerHTML与内容保持同步。我的另一个选择是直接更新innerHTML属性。

基于这个其他SO问题的以下答案,我认为使用dangerouslySetInnerHTML很重要:

React.js: Set innerHTML vs dangerouslySetInnerHTML

  

是的,有区别!

     

使用innerHTML和危险地设置InnerHTML的直接效果是相同的-DOM节点将使用注入的HTML进行更新。

     

但是,当您在危险地使用SetInnerHTML时,它会在幕后使React知道该组件内部的HTML无关紧要。

     

因为React使用了一个虚拟DOM,当它比较diff与实际DOM时,它可以直接绕过检查该节点的子节点,因为它知道HTML来自其他来源。这样就可以提高性能。

     

更重要的是,如果仅使用innerHTML,React无法知道已修改DOM节点。下一次调用render函数时,React将覆盖手动注入的内容它认为该DOM节点的正确状态应该是什么。

但是现在我想出了以下问题:

当有新的输入并且您使用新的HTML设置状态时,您的组件将使用新的dangerouslySetInnerHTML={{ __html: html }}重新渲染,并且插入符号会跳转到位置(在所有经过测试的浏览器上返回到行首到目前为止:Chrome,Firefox,Edge)。

但是这里没有新内容。如果我使用innerHTML设置新的html,则也会出现此插入符号。 dangerouslySetInnerHTML用新的state接收html的问题是,当您设置新的state时,它是对setState()的异步调用。

因此,您无法立即致电placeCaret()。仅当您使用setTimeout()花费一些时间来使新的state更新组件时,此方法才有效。

  setHTML(root.innerHTML);
  setTimeout(() => placeCaret(root.innerText.length), 100); // TIMEOUT NEEDED
  //placeCaret(root.innerText.length);

无论如何,这是一种解决方法,但最终会导致糟糕的UX,因为用户可以看到插入符号在每次击键时都会跳动。

问题

是否有使用dangerouslySetInnerHTML来解决这种插入符跳跃情况的解决方案?还是应该避免使用它,而直接使用innerHTML以便可以立即更改元素?

注意:我认为使用shouldComponentUpdate阻止组件更新不是可行的解决方案。因为有时我实际上必须修改html,例如,将一些文本添加到跨度中,等等。因此,我将需要在不插入符号的情况下更新组件。

Code Sandbox with example

import React, { useRef, useState } from "react";
import ReactDOM from "react-dom";
import styled from "styled-components";

const S = {};

S.div = styled.div`
  border: 1px dotted blue;
`;

function App() {
  const divRef = useRef(null);
  const [html, setHTML] = useState("<div><br></div>");

  function onInput() {
    const root = divRef.current;

    setHTML(root.innerHTML);
    setTimeout(() => placeCaret(root.innerText.length), 100);
    //placeCaret(root.innerText.length);
  }

  // PLACE CARET BACK IN POSITION
  function placeCaret(position) {
    const root = divRef.current;
    const selection = window.getSelection();
    const range = selection.getRangeAt(0);
    range.setStart(root.firstChild.firstChild, position);
  }

  function onKeyDown(e) {
    console.log("On keydown... " + e.key);
    if (e.key === "Enter") {
      e.preventDefault();
    }
  }

  return (
    <React.Fragment>
      <S.div
        contentEditable
        ref={divRef}
        onInput={onInput}
        onKeyDown={onKeyDown}
        dangerouslySetInnerHTML={{ __html: html }}
      />
    </React.Fragment>
  );
}

0 个答案:

没有答案