React.js: onChange event for contentEditable

156,010

Solution 1

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

Solution 2

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>

Solution 3

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.

Solution 4

Since when the edit is complete the focus from the element is always lost you could simply use an onBlur event handler.

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

Solution 5

This probably isn't exactly the answer you're looking for, but having struggled with this myself and having issues with suggested answers, I decided to make it uncontrolled instead.

When editable prop is false, I use text prop as is, but when it is true, I switch to editing mode in which text has no effect (but at least browser doesn't freak out). During this time onChange are fired by the control. Finally, when I change editable back to false, it fills HTML with whatever was passed in 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;
Share:
156,010

Related videos on Youtube

NVI
Author by

NVI

Updated on May 11, 2022

Comments

  • NVI
    NVI almost 2 years

    How do I listen to change event for contentEditable-based control?

    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/

    • Dan Abramov
      Dan Abramov over 9 years
      Having struggled with this myself, and having issues with suggested answers, I decided to make it uncontrolled instead. That is, I put initialValue into state and use it in render, but I don't let React update it further.
    • Green
      Green over 7 years
      Your JSFiddle doesn't work
    • ovidiu-miu
      ovidiu-miu over 4 years
      I avoided struggling with contentEditable by changing my approach - instead of a span or paragraph, I've used an input along with its readonly attribute.
  • Brigand
    Brigand about 10 years
    @NVI, it's the shouldComponentUpdate method. It'll only jump if the html prop is out of sync with the actual html in the element. e.g. if you did this.setState({html: "something not in the editable div"}})
  • Sebastien Lorber
    Sebastien Lorber almost 10 years
    nice but I guess the call to this.getDOMNode().innerHTML in shouldComponentUpdate is not very optimized right
  • Brigand
    Brigand almost 10 years
    @SebastienLorber not very optimized, but I'm pretty sure it's better to read the html, than to set it. The only other option I can think of is to listen to all events that could change the html, and when those happen you cache the html. That'd probably be faster most of the time, but add a lot of complexity. This is the very sure and simple solution.
  • univerio
    univerio almost 10 years
    This is actually slightly flawed when you want to set state.html to the last "known" value, React will not update the DOM because the new html is exactly the same as far as React is concerned (even though the actual DOM is different). See jsfiddle. I have not found a good solution for this, so any ideas are welcome.
  • dchest
    dchest almost 10 years
    @univerio what about setting DOM's innerHTML right inside shouldComponentUpdate? See this jsfiddle.
  • Brigand
    Brigand almost 10 years
    @dchest shouldComponentUpdate should be pure (not have side effects).
  • Sebastien Lorber
    Sebastien Lorber almost 10 years
    Using this.lastHtml is bad because you assign a variable to the component class and not to its instance state
  • Brigand
    Brigand almost 10 years
    @SebastienLorber I believe this demo disproves that. Let me know if I'm missing something. If this.x was shared, they'd display the same number after "id:".
  • Sebastien Lorber
    Sebastien Lorber almost 10 years
    I find your exemple quite complex but it seems you are right: jsfiddle.net/4TpnG/300 however I didn't see anywhere in the doc that it was recommended to add things in this instead of the state
  • Leastrio
    Leastrio over 9 years
    @SebastienLorber right on the front page of the react docs they show this pattern. The "stateful component" example stores the timer's id on the instance directly: this.interval = setInterval(this.tick, 1000);
  • NVI
    NVI over 9 years
    Could you expand on the issues you were having with the other answers?
  • Dan Abramov
    Dan Abramov over 9 years
    @NVI: I need safety from injection, so putting HTML as is is not an option. If I don't put HTML and use textContent, I get all sorts of browser inconsistencies and can't implement shouldComponentUpdate so easily so even it doesn't save me from caret jumps anymore. Finally, I have CSS pseudo-element :empty:before placeholders but this shouldComponentUpdate implementation prevented FF and Safari from cleaning up the field when it is cleared by user. Took me 5 hours to realize I can sidestep all these problems with uncontrolled CE.
  • NVI
    NVI over 9 years
    I don’t quite understand how it works. You never change editable in UncontrolledContentEditable. Could you provide a runnable example?
  • Dan Abramov
    Dan Abramov over 9 years
    @NVI: It's a bit hard since I use a React internal module here.. Basically I set editable from outside. Think a field that can be edited inline when user presses “Edit” and should be again readonly when user presses “Save” or “Cancel”. So when it is readonly, I use props, but I stop looking at them whenever I enter “edit mode” and only look at props again when I exit it.
  • Dan Abramov
    Dan Abramov over 9 years
    So this will only work if parent component can provide text, editable and onChange. It does not support changing text from outside while editable is true—but you rarely need this anyway.
  • Sebastien Lorber
    Sebastien Lorber over 9 years
    @FakeRainBrigand I've posted a new answer which fixes a little bug in your implementation: stackoverflow.com/questions/22677931/…
  • Sebastien Lorber
    Sebastien Lorber over 9 years
    @FakeRainBrigand I am back with another question but could you explain why you choose to handle the onBlur event to emit a change?
  • Brigand
    Brigand over 9 years
    IE9 has some bugs with the input event (backspace doesn't trigger it), and I think there was some other case where it isn't called.. the onBlur is intended as a fallback, even if it's a delayed one.
  • Sebastien Lorber
    Sebastien Lorber about 9 years
    The author has credited my SO answer in package.json. This is almost the same code that I posted and I confirm this code works for me. github.com/lovasoa/react-contenteditable/blob/master/…
  • Jen S.
    Jen S. almost 9 years
    I downvoted this and upvoted Sebastien's answer so that it rises to the top. I hope that is the correct process to follow.
  • Luca Colonnello
    Luca Colonnello almost 9 years
    I've implemented keyPress version that alert the text when enter key is pressed. jsfiddle.net/kb3gN/11378
  • Sebastien Lorber
    Sebastien Lorber almost 9 years
    @LucaColonnello you'd better use {...this.props} so that the client can customize this behavior from the outside
  • Luca Colonnello
    Luca Colonnello almost 9 years
    Oh yeah, this is better! Honestly I have tried this solution only to check if the keyPress event working on div! Thanks for clarifications
  • wuct
    wuct almost 9 years
    For whom you are going to use this code, React has renamed escapeTextForBrowser to escapeTextContentForBrowser.
  • kmoe
    kmoe over 8 years
    Could you explain how the shouldComponentUpdate code prevents caret jumps?
  • Sebastien Lorber
    Sebastien Lorber over 8 years
    @kmoe because the component never updates if the contentEditable already has the appropriate text (ie on keystroke). Updating the contentEditable with React makes the caret jump. Try without contentEditable and see yourself ;)
  • polkovnikov.ph
    polkovnikov.ph almost 8 years
    Wasn't documentation absolutely explicit on never ever using dangerouslySetInnerHTML={{__html: ...}}? Cannot user just copy-paste some <script> or <img onerror="some.evil.magic()"> from third-party website? I see no single word on filtering such things here.
  • tomericco
    tomericco almost 8 years
    Nice, but won't work in IE11. onInput event for contentEditable div is not working there: jsfiddle.net/dbmu8yps
  • geoffliu
    geoffliu almost 8 years
    @SebastienLorber Alternative fix to your problem is to shove the HTML from props into a detached container, and then read the innerHTML out, before you do any comparison. See github.com/lovasoa/react-contenteditable/pull/26/files
  • clint
    clint almost 7 years
    I find this answer to be the most succinct and helpful. Some folks might also want to check out Facebook's draft-js component which does all this and way more.
  • Herick
    Herick over 6 years
    You'll likely want to set this.lastHtml somewhere in a constructor if you're going for this approach, otherwise it will fire a change event whenever you trigger blur even though you haven't changed anything yet.
  • Eric Walker
    Eric Walker about 6 years
    I was having little luck getting the approaches in the higher-voted answers to work with tribute.js, and I took your component, converted it to ES6, fixed a bug or two, and things seem to work.
  • trysis
    trysis almost 6 years
    Instead of preventing caret jumps using shouldComponentUpdate, I'm leaning toward manually re-setting the caret in componentDidUpdate. I found the awesome selection DOM API to help with that. The entire element gets re-rendered, but the caret stays where it is (technically "goes back" to where it was). I am still having trouble with some weird input range bug.
  • Sebastian Thomas
    Sebastian Thomas about 5 years
    No need to downvote this, it works! Just remember to use onInput as stated in example.
  • JulienRioux
    JulienRioux over 4 years
    Nice and clean, I hope it works on many devices and browsers!
  • Daniel says Reinstate Monica
    Daniel says Reinstate Monica about 4 years
    Cool implementation. Your JSFiddle link seems to have an issue.
  • ton1
    ton1 about 4 years
    It move caret to beginning of text constantly when I update text with React state.
  • Ufenei augustine
    Ufenei augustine about 3 years
    This solution will strip out all the markup and give you just the text content defeating the reason why the content editable div is used. Rather use innerHTML i.e onInput={(e) =>console.log("Text inside div", e.currentTarget.innerHTML) }
  • Umang
    Umang almost 3 years
    This works but as @JuntaeKim suggested, the caret always stays at the beginning and does not change it's position. Any ideas on how to change position of caret?
  • BobtheMagicMoose
    BobtheMagicMoose almost 3 years
    @Umang Is react updating the state within the div? If you modify state from the div and then the div is updated based on that state, it replaced the DOM element (I believe) which removes the caret. You'll need to find a way to avoid having react insert state into the div. I just made my own functions to manage state outside of react
  • ericgio
    ericgio almost 3 years
    This works great if you need an uncontrolled component. It doesn't work well for controlled situations, as others have mentioned, but React also discourages this with a warning: 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.
  • ggorlen
    ggorlen over 2 years
    How about using the original approach but storing e.currentTarget.textContent in a string variable, e.g. const {textContent} = e.currentTarget, then use that variable to set state? This won't go stale as the object property might.
  • ggorlen
    ggorlen over 2 years
    I would leave the component uncontrolled and use onBlur/onInput rather than attempting to control the value and cursor position with React.
  • vsync
    vsync almost 2 years
    This answers just a single scenario. There is still a need to act upon content change (for example, update custom scrollbars while typing)