React.js: onChange event for contentEditable React.js: onChange event for contentEditable javascript javascript

React.js: onChange event for contentEditable


Edit: See Sebastien Lorber's answer which fixes a bug in my implementation.


Use the onInput event, and optionally onBlur as a fallback. You might want to save the previous contents to prevent sending extra events.

I'd personally have this as my render function.

var handleChange = function(event){    this.setState({html: event.target.value});}.bind(this);return (<ContentEditable html={this.state.html} onChange={handleChange} />);

jsbin

Which uses this simple wrapper around 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;    }});


Edit 2015

Someone has made a project on NPM with my solution: https://github.com/lovasoa/react-contenteditable

Edit 06/2016: I've just encoutered a new problem that occurs when the browser tries to "reformat" the html you just gave him, leading to component always re-rendering. See

Edit 07/2016: here's my production contentEditable implementation. It has some additional options over react-contenteditable that you might want, including:

  • locking
  • imperative API allowing to embed html fragments
  • ability to reformat the content

Summary:

FakeRainBrigand's solution has worked quite fine for me for some time until I got new problems. ContentEditables are a pain, and are not really easy to deal with React...

This JSFiddle demonstrates the problem.

As you can see, when you type some characters and click on Clear, the content is not cleared. This is because we try to reset the contenteditable to the last known virtual dom value.

So it seems that:

  • You need shouldComponentUpdate to prevent caret position jumps
  • You can't rely on React's VDOM diffing algorithm if you use shouldComponentUpdate this way.

So you need an extra line so that whenever shouldComponentUpdate returns yes, you are sure the DOM content is actually updated.

So the version here adds a componentDidUpdate and becomes:

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

The Virtual dom stays outdated, and it may not be the most efficient code, but at least it does work :) My bug is resolved


Details:

1) If you put shouldComponentUpdate to avoid caret jumps, then the contenteditable never rerenders (at least on keystrokes)

2) If the component never rerenders on key stroke, then React keeps an outdated virtual dom for this contenteditable.

3) If React keeps an outdated version of the contenteditable in its virtual dom tree, then if you try to reset the contenteditable to the value outdated in the virtual dom, then during the virtual dom diff, React will compute that there are no changes to apply to the DOM!

This happens mostly when:

  • you have an empty contenteditable initially (shouldComponentUpdate=true,prop="",previous vdom=N/A),
  • the user types some text and you prevent renderings (shouldComponentUpdate=false,prop=text,previous vdom="")
  • after user clicks a validation button, you want to empty that field (shouldComponentUpdate=false,prop="",previous vdom="")
  • as both the newly produced and old vdom are "", React does not touch the dom.


This is the is simplest solution that worked for me.

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