React.js:contentEditable的onChange事件


120

如何监听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/


11
自己为此苦苦挣扎,并且对建议的答案有疑问,因此我决定使其不受控制。也就是说,我把initialValue进入state和使用它render,但我不让进一步反应更新它。
Dan Abramov

您的jsfiddle不工作
格林

contentEditable通过更改方法来避免麻烦-代替了spanor paragraph,我使用了inputwith及其readonly属性。
ovidiu-miu

Answers:


79

编辑:请参阅Sebastien Lorber的答案,该答案修复了我的实现中的错误。


使用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;
    }
});

1
@NVI,这是shouldComponentUpdate方法。仅当html属性与元素中的实际html不同步时,它才会跳转。例如,如果您做了this.setState({html: "something not in the editable div"}})
Brigand 2014年

1
不错,但我想呼吁this.getDOMNode().innerHTMLshouldComponentUpdate是不是很优化权
塞巴斯蒂安·洛伯

@SebastienLorber并不是非常优化,但是我敢肯定,阅读HTML比设置HTML更好。我唯一想到的另一种选择是监听所有可能更改html的事件,并在发生这些事件时缓存html。在大多数时候,这可能会更快,但是会增加很多复杂性。这是非常肯定和简单的解决方案。
Brigand 2014年

3
当您想将其设置state.html为最后一个“已知”值时,这实际上是有缺陷的,React不会更新DOM,因为就React而言,新的html完全相同(即使实际DOM不同)。参见jsfiddle。我还没有找到一个好的解决方案,因此欢迎提出任何想法。
univerio 2014年

1
@dchest shouldComponentUpdate应该是纯净的(没有副作用)。
Brigand

66

编辑2015

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

编辑06/2016:我刚刚遇到了一个新问题,当浏览器尝试“重新格式化”刚刚给他的html时,会导致组件始终重新呈现,这会发生一个新问题。看到

编辑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;
    }
});

虚拟dom仍然过时,并且它可能不是最有效的代码,但至少它确实有效:) 我的错误已解决


细节:

1)如果放置了shouldComponentUpdate以避免插入符号跳动,则contenteditable永不放弃(至少在击键时)

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

3)如果React在其虚拟dom树中保留了contenteditable的过时版本,那么如果您尝试将contenteditable重置为虚拟dom中过时的值,那么在虚拟dom diff期间,React将计算出对申请DOM!

这种情况通常发生在以下情况:

  • 您最初具有一个可编辑的空内容(shouldComponentUpdate = true,prop =“”,先前的vdom = N / A),
  • 用户键入一些文本,然后阻止渲染(shouldComponentUpdate = false,prop = text,先前的vdom =“”)
  • 用户单击验证按钮后,您想清空该字段(shouldComponentUpdate = false,prop =“”,上一个vdom =“”)
  • 由于新产生的vdom和旧的vdom均为“”,因此React不会触及dom。

1
我已经实现了keyPress版本,该版本可以在按下Enter键时提醒文本。jsfiddle.net/kb3gN/11378
Luca Colonnello

您最好使用@LucaColonnello,{...this.props}以便客户端可以从外部自定义此行为
Sebastien Lorber 2015年

哦,是的,这更好!老实说,我只是尝试过此解决方案,以检查keyPress事件是否在div上起作用!感谢澄清
卢卡Colonnello意

1
您能解释一下shouldComponentUpdate代码如何防止插入符号跳转吗?
kmoe 2015年

1
@kmoe,因为如果contentEditable已经具有适当的文本(例如,在击键时),则该组件永远不会更新。使用React更新contentEditable会使插入符号跳转。尝试在没有contentEditable的情况下尝试一下;)
塞巴斯蒂安·洛伯

28

这是最适合我的最简单的解决方案。

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

3
无需对此投票,它可以工作!只要记住onInput按照示例中的说明使用即可。
塞巴斯蒂安·托马斯

干净整洁,我希望它可以在许多设备和浏览器上运行!
JulienRioux

7
当我用React状态更新文本时,它将插入符号不断地移动到文本的开头。
Juntae

18

这可能不完全是您要寻找的答案,但是我自己为此苦苦挣扎,并对建议的答案有疑问,因此我决定使它不受控制。

editableprop是时false,我text原样使用prop,但是当它是时true,我切换到text没有任何作用的编辑模式(但至少浏览器不会出现异常)。在这段时间onChange内被控件触发。最后,当我改editable回时false,它将使用传入的内容填充HTML text

/** @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 2014年

1
@NVI:我需要防止注入,因此不能按原样放置HTML。如果我不使用HTML并使用textContent,那么我会遇到各种浏览器不一致的情况,并且实现shouldComponentUpdate起来不那么容易,因此即使它也不再使我免于插入符号跳转的麻烦。最后,我有CSS伪元素:empty:before占位符,但此shouldComponentUpdate实现阻止FF和Safari在用户清除字段时清除字段。花了我5个小时才意识到我可以通过不受控制的CE来回避所有这些问题。
Dan Abramov

我不太了解它是如何工作的。你永远不会改变editableUncontrolledContentEditable。您能否提供一个可运行的示例?
NVI 2014年

@NVI:这有点困难,因为我在这里使用React内部模块。基本上我是editable从外部设置的。考虑一个当用户按下“编辑”时可以内联编辑的字段,当用户按下“保存”或“取消”时该字段应再次为只读。因此,当它是只读的时,我会使用道具,但是当我进入“编辑模式”时,我会停止查看它们,而仅在退出它时再次查看它们。
Dan Abramov

3
对于要使用此代码的用户,React已重命名escapeTextForBrowserescapeTextContentForBrowser
wuct

8

由于完成编辑后,总是会失去元素的焦点,因此您可以简单地使用onBlur挂钩。

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

5

我建议使用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;
    }
}

2

这是由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;
}

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


1
作者将我的SO答案归功于package.json。这几乎与我发布的代码相同,我确认此代码对我有用。github.com/lovasoa/react-contenteditable/blob/master/…–
Sebastien Lorber
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.