React contentEditable and cursor position

Viewed 13181

I have simple component

class ContentEditable extends React.Component {
  constructor(props) {
    super(props);
    this.handleInput = this.handleInput.bind(this);
  }

  handleInput(event) {
    let html = event.target.innerHTML;
    if (this.props.onChange && html !== this.lastHtml) {
      this.props.onChange({ target: { value: html, name: this.props.name } });
      this.lastHtml = html;
    }
  }

  render() {
    return (
      <span
        contentEditable="true"
        onInput={this.handleInput}
        className={"auto " + this.props.className}
        dangerouslySetInnerHTML={{ __html: this.props.value }}
      />
    );
  }
}
export default ContentEditable;

<ContentEditable
  value={this.state.name}
  onChange={e => {
    this.setState({ name: e.target.value });
  }}
/>;

The component works but the cursor position never changes, it is always on first position instead after the rendered text.

I tested examples form this forum but it doesn't work for me.

I use React 15.6.1 and test it on chrome (Os X).

Any hint how I can solve this problem?

3 Answers

The solution with useRef will be something look like below.

keep the data within custom input with useRef

Here the useRef will keep the default value / initial value apart from the component rendering cycles, so it will retain the original value without being affected by other kinds of operations we do in the react component.

This component does two things

  1. This will emit the user input to the parent component with an onChange method
  2. Takes a default value from parent component as prop named value and renders the value in the custom input box (that was created using contentEditable)

I have added a code sandbox, link here, use this to see how this works!

The code sandbox example contains two components

  • one is ContentEditableWithRef which solves the problem with useRef , which is an uncontrolled component and
  • the other component is ContentEditable which uses useState to solve the same problem.

I also had same problem. Just fixed it with ref. Just assign textContent of event.target to ref.

const textareaEl = useRef<HTMLDivElement>(null);

const handleChange = (e: React.ChangeEvent<HTMLDivElement>) => {
    textareaEl.current.textContent = e.target.textContent;
    onChange(e); // If you have change event for form/state
};

/** If you're passing value from state, 
    you can mutate it each change for not losing cursor position. 
 */
useEffect(() => {
  if (value) {
     textareaEl.current.textContent = value;
  }
}, [value]);


return (
    <div
      id="textarea-element"
      ref={textareaEl}
      contentEditable={true}
      suppressContentEditableWarning={true}
      onChange={handleChange}
     />
)

import React, { useRef } from 'react';

const ContentEditable = ({ value, onChange = () => {}, ...props }) => {
    const state = useRef({ value, prevValue: null, key: null });

    if (state.current.prevValue !== value) {
        state.current.value = value;
        state.current.key = Date.now();
    }

    const _handleInput = (event) => {
        const value = event.target.innerText;
        state.current.prevValue = value;
        onChange(value);
    };

    return (
        <div
            {...props}
            key={state.current.key}
            role="textbox"
            contentEditable={true}
            dangerouslySetInnerHTML={{ __html: state.current.value }}
            onInput={_handleInput}
        />
    );
};
export default ContentEditable;

usage:

const [value, setValue] = useState("initial test");

<ContentEditable
  value={value}
  onChange={setValue}
/>
Related