187

How do I listen to change events for a 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);

Code on JSFiddle.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
NVI
  • 14,907
  • 16
  • 65
  • 104
  • 20
    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. – Dan Abramov Oct 02 '14 at 17:30
  • 1
    Your JSFiddle doesn't work – Green Nov 30 '16 at 21:06
  • 1
    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. – ovidiu-miu Sep 08 '19 at 11:20

11 Answers11

118

This is the simplest solution that worked for me.

<div
  contentEditable='true'
  onInput={e => console.log('Text inside div', e.currentTarget.textContent)}
>
Text inside div
</div>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Abhishek Kanthed
  • 1,744
  • 1
  • 11
  • 15
  • 55
    It move caret to beginning of text constantly when I update text with React state. – ton1 Mar 04 '20 at 09:43
  • 1
    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) }` – Ufenei augustine Feb 03 '21 at 23:35
  • 1
    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? – Umang May 12 '21 at 07:28
  • @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 – BobtheMagicMoose May 28 '21 at 01:03
  • 4
    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.` – ericgio Jul 20 '21 at 05:59
  • I would leave the component uncontrolled and use `onBlur`/`onInput` rather than attempting to control the value and cursor position with React. – ggorlen Dec 11 '21 at 22:17
99

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;
    }
});
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Brigand
  • 84,529
  • 20
  • 165
  • 173
  • 1
    @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"}})` – Brigand Mar 27 '14 at 06:21
  • 1
    nice but I guess the call to `this.getDOMNode().innerHTML` in `shouldComponentUpdate` is not very optimized right – Sebastien Lorber Jun 28 '14 at 14:01
  • @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. – Brigand Jun 28 '14 at 15:58
  • 3
    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](http://jsfiddle.net/vSP3s/1/). I have not found a good solution for this, so any ideas are welcome. – univerio Jun 29 '14 at 00:46
  • @univerio what about setting DOM's `innerHTML` right inside `shouldComponentUpdate`? See [this jsfiddle](http://jsfiddle.net/vSP3s/3/). – dchest Jun 29 '14 at 11:13
  • 1
    @dchest `shouldComponentUpdate` should be pure (not have side effects). – Brigand Jun 29 '14 at 18:51
  • Using `this.lastHtml` is bad because you assign a variable to the component class and not to its instance state – Sebastien Lorber Jul 18 '14 at 13:52
  • @SebastienLorber I believe [this demo](http://jsbin.com/muduwemo/2/edit?js,output) disproves that. Let me know if I'm missing something. If `this.x` was shared, they'd display the same number after "id:". – Brigand Jul 19 '14 at 04:16
  • I find your exemple quite complex but it seems you are right: http://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 – Sebastien Lorber Jul 19 '14 at 13:46
  • @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);` – WickyNilliams Oct 02 '14 at 13:18
  • @FakeRainBrigand I've posted a new answer which fixes a little bug in your implementation: http://stackoverflow.com/questions/22677931/react-js-onchange-event-for-contenteditable/27255103#27255103 – Sebastien Lorber Dec 02 '14 at 17:13
  • @FakeRainBrigand I am back with another question but could you explain why you choose to handle the onBlur event to emit a change? – Sebastien Lorber Dec 03 '14 at 09:52
  • 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. – Brigand Dec 03 '14 at 15:58
  • I downvoted this and upvoted Sebastien's answer so that it rises to the top. I hope that is the correct process to follow. – Jen S. May 19 '15 at 06:34
  • Wasn't documentation absolutely explicit on __never ever using__ `dangerouslySetInnerHTML={{__html: ...}}`? Cannot user just copy-paste some ` – polkovnikov.ph Jun 02 '16 at 20:22
  • Nice, but won't work in IE11. onInput event for contentEditable div is not working there: https://jsfiddle.net/dbmu8yps/ – tomericco Jul 14 '16 at 12:01
84

Someone has made a project on NPM with my solution: react-contenteditable

I've encountered another problem that occurs when the browser tries to "reformat" the HTML you just gave it, leading to component always rerendering. See this.

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 virtual DOM are "", React does not touch the DOM.
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Sebastien Lorber
  • 89,644
  • 67
  • 288
  • 419
  • 1
    I've implemented keyPress version that alert the text when enter key is pressed. http://jsfiddle.net/kb3gN/11378/ – Luca Colonnello Jun 11 '15 at 12:31
  • @LucaColonnello you'd better use `{...this.props}` so that the client can customize this behavior from the outside – Sebastien Lorber Jun 11 '15 at 12:49
  • Oh yeah, this is better! Honestly I have tried this solution only to check if the keyPress event working on div! Thanks for clarifications – Luca Colonnello Jun 11 '15 at 12:50
  • 1
    Could you explain how the `shouldComponentUpdate` code prevents caret jumps? – kmoe Aug 19 '15 at 11:56
  • 1
    @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 ;) – Sebastien Lorber Aug 19 '15 at 13:09
  • @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 https://github.com/lovasoa/react-contenteditable/pull/26/files – geoffliu Jul 27 '16 at 16:08
  • 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. – Herick Oct 05 '17 at 19:01
  • 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. – trysis Jun 23 '18 at 20:06
  • 2
    Cool implementation. Your JSFiddle link seems to have an issue. – Daniel Node.js Feb 12 '20 at 17:39
36

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>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Saint Laurent
  • 461
  • 4
  • 3
21

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;
Alireza
  • 100,211
  • 27
  • 269
  • 172
Dan Abramov
  • 264,556
  • 84
  • 409
  • 511
  • Could you expand on the issues you were having with the other answers? – NVI Oct 02 '14 at 18:44
  • 1
    @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. – Dan Abramov Oct 02 '14 at 18:47
  • I don’t quite understand how it works. You never change `editable` in `UncontrolledContentEditable`. Could you provide a runnable example? – NVI Oct 02 '14 at 18:54
  • @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 Oct 02 '14 at 18:57
  • 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. – Dan Abramov Oct 02 '14 at 18:58
  • 3
    For whom you are going to use this code, React has renamed `escapeTextForBrowser` to `escapeTextContentForBrowser`. – wuct Jun 17 '15 at 06:17
  • I find this answer to be the most succinct and helpful. Some folks might also want to check out Facebook's [draft-js](https://draftjs.org/) component which does all this and _way_ more. – clint May 02 '17 at 19:46
  • 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. – Eric Walker Apr 18 '18 at 22:42
6

I suggest using a MutationObserver to do this. It gives you a lot more control over what is going on. It also gives you more details on how the browse interprets all the keystrokes.

Here in 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;
    }
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
klugjo
  • 19,422
  • 8
  • 57
  • 75
2

Here is a component that incorporates much of this by lovasoa: https://github.com/lovasoa/react-contenteditable/blob/master/src/react-contenteditable.tsx#L97

He shims the event in the 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;
}

I'm using a similar approach successfully

ADTC
  • 8,999
  • 5
  • 68
  • 93
Jeff
  • 939
  • 10
  • 19
  • 1
    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. https://github.com/lovasoa/react-contenteditable/blob/master/package.json – Sebastien Lorber Apr 15 '15 at 13:31
  • The link is broken (*"404. Page not found"*). – Peter Mortensen Sep 16 '22 at 16:07
  • @PeterMortensen I updated the link based on what I could find in the repo. The code has changed slightly but it looks like the idea remains the same. – ADTC Aug 25 '23 at 08:55
2
<div
    spellCheck="false"
    onInput={e => console.log("e: ", e.currentTarget.textContent}
    contentEditable="true"
    suppressContentEditableWarning={true}
    placeholder="Title"
    className="new-post-title"
/>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Hoang Long
  • 446
  • 4
  • 5
  • 1
    An explanation would be in order. E.g., what is the idea/gist? What was it tested on (browser, operating system), incl. versions (incl. React). From [the Help Center](https://stackoverflow.com/help/promotion): *"...always explain why the solution you're presenting is appropriate and how it works"*. Please respond by [editing (changing) your answer](https://stackoverflow.com/posts/72195285/edit), not here in comments (****************************** ***without*** *********************** "Edit:", "Update:", or similar - the answer should appear as if it was written today). – Peter Mortensen Sep 16 '22 at 16:17
  • A "`)`" seems to be missing near `"console.log("e: ", e.currentTarget.textContent"`. – Peter Mortensen Sep 16 '22 at 16:24
0

Here's my hooks-based version based on Sebastien Lorber's answer:

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>
  );
};
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
neurosnap
  • 5,658
  • 4
  • 26
  • 30
  • i see a perfect vector for XSS here guys, i suppose this is not meant as a serious answer suitable for production. at least you could have mentioned the need for some sanitisation that might or might _not_ happen in the parent component. the best practice is to avoid using `dangerouslySetInnerHTML` all together. see 'React’s dangerouslySetInnerHTML without sanitising the HTML' [here](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) – okram Jul 08 '23 at 10:51
0

I'm done with onInput. Thank You

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="//unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
 div{
  border: .2px solid black;
 }
</style>
# editable
<div id="root"></div>

<script type="text/babel">
const { React, ReactDOM} = window;

const App =()=>(
    <div 
       contentEditable={true}
       onInput={(e) => {
         console.log(e.target.innerText);
        }}
     >Edit me</div>
)
ReactDOM.render(<App/>, document.querySelector('#root'));
</script>
perona chan
  • 101
  • 1
  • 8
-1

I've tried to use the example from Saint Laurent:

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

It's working perfectly until I've tried to set this value in state. When I'm using a functional component that calls setState(e.currentTarget.textContent), I'm getting currentTarget as null. setState works asynchronously and currentTarget is not available there.

The fix that worked for me in React 17.0.2 was to use e.target.innerText:

<div
  onBlur={e => setState(e.target.innerText)}
  contentEditable
  suppressContentEditableWarning={true}
>
    <p>Lorem ipsum dolor.</p>
</div>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Maksym Popov
  • 85
  • 1
  • 5
  • 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 Dec 11 '21 at 21:17