【问题标题】:React.js: onChange event for contentEditableReact.js:contentEditable 的 onChange 事件
【发布时间】:2014-05-05 20:30:56
【问题描述】:

如何监听基于contentEditable 的控件的更改事件?

var Number = React.createClass({
    render: function() {
        return <div>
            <span contentEditable={true} onChange={this.onChange}>
                {this.state.value}
            </span>
            =
            {this.state.value}
        </div>;
    },
    onChange: function(v) {
        // Doesn't fire :(
        console.log('changed', v);
    },
    getInitialState: function() {
        return {value: '123'}
    }    
});

React.renderComponent(<Number />, document.body);

http://jsfiddle.net/NV/kb3gN/1621/

【问题讨论】:

  • 我自己也在为此苦苦挣扎,并且对建议的答案有疑问,所以我决定让它不受控制。也就是我把initialValue放到state里面用render,但是我不让React进一步更新。
  • 您的 JSFiddle 不起作用
  • 我通过改变我的方法避免了与contentEditable 的斗争——我使用了input 及其readonly 属性,而不是spanparagraph

标签: javascript dom contenteditable reactjs


【解决方案1】:

编辑:请参阅Sebastien Lorber's answer,它修复了我的实现中的错误。


使用 onInput 事件,并可选择使用 onBlur 作为后备。您可能希望保存以前的内容以防止发送额外的事件。

我个人会将此作为我的渲染函数。

var handleChange = function(event){
    this.setState({html: event.target.value});
}.bind(this);

return (<ContentEditable html={this.state.html} onChange={handleChange} />);

jsbin

它使用这个简单的 contentEditable 包装器。

var ContentEditable = React.createClass({
    render: function(){
        return <div 
            onInput={this.emitChange} 
            onBlur={this.emitChange}
            contentEditable
            dangerouslySetInnerHTML={{__html: this.props.html}}></div>;
    },
    shouldComponentUpdate: function(nextProps){
        return nextProps.html !== this.getDOMNode().innerHTML;
    },
    emitChange: function(){
        var html = this.getDOMNode().innerHTML;
        if (this.props.onChange && html !== this.lastHtml) {

            this.props.onChange({
                target: {
                    value: html
                }
            });
        }
        this.lastHtml = html;
    }
});

【讨论】:

  • @NVI,它是 shouldComponentUpdate 方法。只有当 html 属性与元素中的实际 html 不同步时,它才会跳转。例如如果你做了this.setState({html: "something not in the editable div"}})
  • 不错,但我猜shouldComponentUpdate 中对this.getDOMNode().innerHTML 的调用不是很优化
  • @SebastienLorber 没有非常优化,但我很确定阅读 html 比设置它更好。我能想到的唯一其他选择是监听所有可能更改 html 的事件,当这些事件发生时,您缓存 html。大多数时候这可能会更快,但会增加很多复杂性。这是非常确定和简单的解决方案。
  • 当你想将state.html 设置为最后一个“已知”值时,这实际上有点缺陷,React 不会更新 DOM,因为就 React 而言,新的 html 完全相同(即使实际的 DOM 不同)。见jsfiddle。我还没有找到一个好的解决方案,所以欢迎任何想法。
  • @dchest shouldComponentUpdate 应该是纯的(没有副作用)。
【解决方案2】:

2015 年编辑

有人用我的解决方案在 NPM 上做了一个项目:https://github.com/lovasoa/react-contenteditable

编辑 06/2016: 我刚刚遇到了一个新问题,当浏览器尝试“重新格式化”您刚刚提供给他的 html 时会出现该问题,从而导致组件总是重新呈现。 See

Edit 07/2016:这是我的生产 contentEditable 实现。除了react-contenteditable,它还有一些您可能想要的附加选项,包括:

  • 锁定
  • 命令式 API 允许嵌入 html 片段
  • 能够重新格式化内容

总结:

FakeRainBrigand 的解决方案在我遇到新问题之前一直很好用。 ContentEditables 很痛苦,而且用 React 处理起来并不容易......

这个JSFiddle 说明了问题。

如你所见,当你输入一些字符并点击Clear时,内容并没有被清除。这是因为我们尝试将 contenteditable 重置为最后一个已知的虚拟 dom 值。

看来:

  • 您需要shouldComponentUpdate 来防止插入符号位置跳转
  • 如果你以这种方式使用shouldComponentUpdate,你就不能依赖 React 的 VDOM 差异算法。

所以你需要一个额外的行,这样每当shouldComponentUpdate 返回yes 时,你就可以确定DOM 内容实际上已经更新了。

所以这里的版本增加了componentDidUpdate,变成:

var ContentEditable = React.createClass({
    render: function(){
        return <div id="contenteditable"
            onInput={this.emitChange} 
            onBlur={this.emitChange}
            contentEditable
            dangerouslySetInnerHTML={{__html: this.props.html}}></div>;
    },

    shouldComponentUpdate: function(nextProps){
        return nextProps.html !== this.getDOMNode().innerHTML;
    },

    componentDidUpdate: function() {
        if ( this.props.html !== this.getDOMNode().innerHTML ) {
           this.getDOMNode().innerHTML = this.props.html;
        }
    },

    emitChange: function(){
        var html = this.getDOMNode().innerHTML;
        if (this.props.onChange && html !== this.lastHtml) {
            this.props.onChange({
                target: {
                    value: html
                }
            });
        }
        this.lastHtml = html;
    }
});

Virtual dom 已经过时了,它可能不是最有效的代码,但至少它确实有效 :) My bug is resolved


详情:

1) 如果你设置 shouldComponentUpdate 以避免插入符号跳转,那么 contenteditable 永远不会重新渲染(至少在击键时)

2) 如果组件在击键时从不重新渲染,那么 React 会为这个 contenteditable 保留一个过时的虚拟 dom。

3) 如果 React 在其虚拟 dom 树中保留了过期版本的 contenteditable,那么如果你尝试将 contenteditable 重置为虚拟 dom 中过期的值,那么在虚拟 dom diff 期间,React 将计算出有没有要应用于 DOM 的更改!

这主要发生在:

  • 您最初有一个空的 contenteditable(shouldComponentUpdate=true,prop="",previous vdom=N/A),
  • 用户键入了一些文本,而您阻止了呈现 (shouldComponentUpdate=false,prop=text,previous vdom="")
  • 在用户单击验证按钮后,您希望清空该字段(shouldComponentUpdate=false,prop="",previous vdom="")
  • 由于新生成的和旧的 vdom 都是 "",因此 React 不会触及 dom。

【讨论】:

  • 我已经实现了 keyPress 版本,当按下回车键时会提醒文本。 jsfiddle.net/kb3gN/11378
  • @LucaColonnello 您最好使用{...this.props},以便客户端可以从外部自定义此行为
  • 您能解释一下shouldComponentUpdate 代码如何防止插入符号跳转吗?
  • @kmoe 因为如果 contentEditable 已经有适当的文本(即击键),组件永远不会更新。使用 React 更新 contentEditable 会使插入符号跳转。尝试不使用 contentEditable 并看看你自己 ;)
  • 很酷的实现。您的 JSFiddle 链接似乎有问题。
【解决方案3】:

这是对我有用的最简单的解决方案。

<div
  contentEditable='true'
  onInput={e => console.log('Text inside div', e.currentTarget.textContent)}
>
Text inside div
</div>

【讨论】:

  • 无需对此投反对票,它有效!请记住使用onInput,如示例中所述。
  • 漂亮又干净,我希望它适用于许多设备和浏览器!
  • 当我使用 React 状态更新文本时,它会不断将插入符号移动到文本的开头。
  • 此解决方案将去除所有标记,只为您提供文本内容,从而破坏了使用内容可编辑 div 的原因。而是使用innerHTML,即onInput={(e) =&gt;console.log("Text inside div", e.currentTarget.innerHTML) }
  • 如果您需要一个不受控制的组件,这非常有用。正如其他人所提到的,它不适用于受控情况,但 React 也通过警告不鼓励这样做:A component is `contentEditable` and contains `children` managed by React. It is now your responsibility to guarantee that none of those nodes are unexpectedly modified or duplicated. This is probably not intentional.
【解决方案4】:

由于编辑完成后,元素的焦点总是丢失,您可以简单地使用onBlur 事件处理程序。

<div
  onBlur={e => {
    console.log(e.currentTarget.textContent);
  }} 
  contentEditable
  suppressContentEditableWarning={true}
>
  <p>Lorem ipsum dolor.</p>
</div>

【讨论】:

    【解决方案5】:

    这可能不是您正在寻找的答案,但我自己也在为此苦苦挣扎并且对建议的答案有疑问,所以我决定让它不受控制。

    editable 属性为false 时,我按原样使用text 属性,但是当它是true 时,我切换到text 无效的编辑模式(但至少浏览器没有别吓坏了)。在此期间onChange 被控件触发。最后,当我将editable 改回false 时,它会用text 中传递的任何内容填充HTML:

    /** @jsx React.DOM */
    'use strict';
    
    var React = require('react'),
        escapeTextForBrowser = require('react/lib/escapeTextForBrowser'),
        { PropTypes } = React;
    
    var UncontrolledContentEditable = React.createClass({
      propTypes: {
        component: PropTypes.func,
        onChange: PropTypes.func.isRequired,
        text: PropTypes.string,
        placeholder: PropTypes.string,
        editable: PropTypes.bool
      },
    
      getDefaultProps() {
        return {
          component: React.DOM.div,
          editable: false
        };
      },
    
      getInitialState() {
        return {
          initialText: this.props.text
        };
      },
    
      componentWillReceiveProps(nextProps) {
        if (nextProps.editable && !this.props.editable) {
          this.setState({
            initialText: nextProps.text
          });
        }
      },
    
      componentWillUpdate(nextProps) {
        if (!nextProps.editable && this.props.editable) {
          this.getDOMNode().innerHTML = escapeTextForBrowser(this.state.initialText);
        }
      },
    
      render() {
        var html = escapeTextForBrowser(this.props.editable ?
          this.state.initialText :
          this.props.text
        );
    
        return (
          <this.props.component onInput={this.handleChange}
                                onBlur={this.handleChange}
                                contentEditable={this.props.editable}
                                dangerouslySetInnerHTML={{__html: html}} />
        );
      },
    
      handleChange(e) {
        if (!e.target.textContent.trim().length) {
          e.target.innerHTML = '';
        }
    
        this.props.onChange(e);
      }
    });
    
    module.exports = UncontrolledContentEditable;
    

    【讨论】:

    • 您能否详细说明您在其他答案中遇到的问题?
    • @NVI:我需要防止注入的安全性,因此不能按原样放置 HTML。如果我不使用 HTML 并使用 textContent,我会遇到各种浏览器不一致,并且无法轻松实现 shouldComponentUpdate,因此即使它也无法让我免于插入符号跳转。最后,我有 CSS 伪元素 :empty:before 占位符,但是这个 shouldComponentUpdate 实现阻止了 FF 和 Safari 在用户清除该字段时对其进行清理。我花了 5 个小时才意识到我可以通过不受控制的 CE 来回避所有这些问题。
    • 我不太明白它是如何工作的。你永远不会在UncontrolledContentEditable 中更改editable。你能提供一个可运行的例子吗?
    • @NVI:这有点难,因为我在这里使用了 React 内部模块。基本上我从外部设置了editable。想想当用户按下“编辑”时可以内联编辑的字段,当用户按下“保存”或“取消”时应该再次只读。所以当它是只读的时候,我会使用 props,但是当我进入“编辑模式”时我会停止查看它们,并且只有在退出时才会再次查看 props。
    • 你要为谁使用这段代码,React 已将 escapeTextForBrowser 重命名为 escapeTextContentForBrowser
    【解决方案6】:

    我建议使用 mutationObserver 来执行此操作。它使您可以更好地控制正在发生的事情。它还为您提供了有关浏览器如何解释所有击键的更多详细信息

    在 TypeScript 中

    import * as React from 'react';
    
    export default class Editor extends React.Component {
        private _root: HTMLDivElement; // Ref to the editable div
        private _mutationObserver: MutationObserver; // Modifications observer
        private _innerTextBuffer: string; // Stores the last printed value
    
        public componentDidMount() {
            this._root.contentEditable = "true";
            this._mutationObserver = new MutationObserver(this.onContentChange);
            this._mutationObserver.observe(this._root, {
                childList: true, // To check for new lines
                subtree: true, // To check for nested elements
                characterData: true // To check for text modifications
            });
        }
    
        public render() {
            return (
                <div ref={this.onRootRef}>
                    Modify the text here ...
                </div>
            );
        }
    
        private onContentChange: MutationCallback = (mutations: MutationRecord[]) => {
            mutations.forEach(() => {
                // Get the text from the editable div
                // (Use innerHTML to get the HTML)
                const {innerText} = this._root; 
    
                // Content changed will be triggered several times for one key stroke
                if (!this._innerTextBuffer || this._innerTextBuffer !== innerText) {
                    console.log(innerText); // Call this.setState or this.props.onChange here
                    this._innerTextBuffer = innerText;
                }
            });
        }
    
        private onRootRef = (elt: HTMLDivElement) => {
            this._root = elt;
        }
    }
    

    【讨论】:

      【解决方案7】:

      这是一个包含 lovasoa 大部分内容的组件:https://github.com/lovasoa/react-contenteditable/blob/master/index.js

      他在 emitChange 中填充事件

      emitChange: function(evt){
          var html = this.getDOMNode().innerHTML;
          if (this.props.onChange && html !== this.lastHtml) {
              evt.target = { value: html };
              this.props.onChange(evt);
          }
          this.lastHtml = html;
      }
      

      我正在成功地使用类似的方法

      【讨论】:

      【解决方案8】:

      这是我基于 https://stackoverflow.com/a/27255103 的基于钩子的版本

      const noop = () => {};
      const ContentEditable = ({
        html,
        onChange = noop,
      }: {
        html: string;
        onChange?: (s: string) => any;
      }) => {
        const ref = useRef<HTMLDivElement>(null);
        const lastHtml = useRef<string>('');
      
        const emitChange = () => {
          const curHtml = ref.current?.innerHTML || '';
          if (curHtml !== lastHtml.current) {
            onChange(curHtml);
          }
          lastHtml.current = html;
        };
      
        useEffect(() => {
          if (!ref.current) return;
          if (ref.current.innerHTML === html) return;
          ref.current.innerHTML = html;
        }, [html]);
      
        return (
          <div
            onInput={emitChange}
            contentEditable
            dangerouslySetInnerHTML={{ __html: html }}
            ref={ref}
          ></div>
        );
      };
      

      【讨论】:

        【解决方案9】:

        我尝试使用上面的example from Saint Laurent

        <div
          onBlur={e => {
            console.log(e.currentTarget.textContent);
          }}
          contentEditable
          suppressContentEditableWarning={true}
        >
          <p>Lorem ipsum dolor.</p>
        </div>
        

        在我尝试将此值设置为 state 之前,它一直运行良好。当我使用调用setState(e.currentTarget.textContent) 的功能组件时,我得到currentTarget 为空。 setState 异步工作,currentTarget 在那里不可用。

        在 React 17.0.2 中对我有用的修复是使用 e.target.innerText:

        <div
          onBlur={e => setState(e.target.innerText)}
          contentEditable
          suppressContentEditableWarning={true}
        >
          <p>Lorem ipsum dolor.</p>
        </div>
        

        【讨论】:

        • 如何使用原始方法但将e.currentTarget.textContent 存储在字符串变量中,例如const {textContent} = e.currentTarget,然后用那个变量来设置状态?这不会像对象属性那样过时。
        猜你喜欢
        • 2014-09-23
        • 2021-04-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-11-17
        • 2012-04-09
        相关资源
        最近更新 更多