Shawn
Shawn

Reputation: 429

React - onChange event for contentEditable attribute

I want to use <div onInput={onChange} contentEditable> to save user input like textarea does. I've heard that I need to use onInput listener to trigger fn when input change. The problem is when I update text with React state, It moves the caret to beginning of text constantly

this is my code

import React from "react";

const App = () => {
  const [value, setValue] = React.useState("I am edittable");
  const onChange = (e) => {
    const html = e.target.innerHTML;
    setValue(html);
  };

  return (
    <div onInput={onChange} contentEditable>
      {value}
    </div>
  );
};
export default App;

codesandbox https://codesandbox.io/s/react-editable-cell-pwil6?file=/src/App.js:0-310

how could I fix it ?

Upvotes: 11

Views: 15470

Answers (5)

enchance
enchance

Reputation: 30511

Typescript implementation using React Hook Form 2023.

tldr; Complete code below for those in a hurry.

NOTE: The code below is a simplified version of a much longer piece which uses zod. To keep things simple I deleted some parts.

export const EmailForm: React.FC = () => {
    const {register, handleSubmit, formState: {errors}, setValue} = useForm();

    // Optional: for when the <label> gets clicked
    const emailRef = useRef<HTMLDivElement>(null);

    const onSubmit = async (data: FieldValues) => {
        // Send data here. In catch statement use setError or formError
        console.log(data);
    }

    const onInput = (e: FormEvent<HTMLInputElement>) => {
        setValue('email', e.currentTarget.innerText);
    }

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            {/* The actual field which react hook form validates */}
            <input type="hidden" {...register('email')} />

            <div>
                <label onClick={() => emailRef.current?.focus()}>Email</label>
                <div contentEditable onInput={onInput} ref={emailRef}
                     role="textbox" tabIndex={0}></div>
                <div>{errors.email?.message}</div>
            </div>
            <div>
                <button type="submit">Submit</button>
            </div>
        </form>
    );
}

Essentially, what you want is to make the div act like a <textarea> while keeping the functionality. The benefits of this varies but I usually do it so it expands automatically while the user types.

Register with React Hook Form

Since this is React, I'm using RHF to manage the content so there's a hidden field we need to populate while the user types. This can also be a <textarea> but just make sure it's invisible.

<input type="hidden" {...register('email')} />

Handling data

A <div> has no knowledge of what an onChange event is but it responds well to the onInput event so use that instead. Also add role and tabIndex to assist in usablity as seen in this so.

<label onClick={() => emailRef.current?.focus()}>Email</label>
<div contentEditable onInput={onInput} ref={emailRef}
     role="textbox" tabIndex={0}></div>

Optionally, if you want to make the <label> focus on the <div> when clicked like a label should then use useRef to get it working:

const emailRef = useRef<HTMLDivElement>(null);

With the onInput={onInput} callback in place, now everytime the user types something the setValue function gets called allowing RHF access to the data as if it were any other field. This in turn assists in generating error messages:

<div>{errors.email?.message}</div>

Lastly, when the form is submitted the onSubmit callback is triggered. From there you can send it to your server.

Upvotes: 4

thewebtud
thewebtud

Reputation: 584

contentEditable will make the content inside div editable.

Instead of using onInput you can use onBlur so that the blinking cursor will not move to beginning of text constantly.

Whatever change you make to the text will be visible on the screen and the state will be updated once you move the focus out of the element.

<div onBlur={onChange} contentEditable>
  {value}
</div>

Upvotes: 2

Steven Smith
Steven Smith

Reputation: 19

This is old, but I just solved this for myself, and I thought I'd share it.

onChange will never fire, and onInput isn't the best way. You need to simulate onChange with onFocus and onBlur.

Copy the value of the innerHTML to a variable when onFocus occurs.

When onBlur occurs, test innerHTML against the variable to see if it changed. If so, do your onChange stuff. You need something like this:

  const onFocus = (e) => {
    val_start = e.target.innerHTML;
  };
  const onBlur = (e) => {
    if(val_start !== e.target.innerHTML) {
      //Whatever you put here will act just like an onChange event
      const html = e.target.innerHTML;
      setValue(html);
    }
  };

Upvotes: 1

Dimitar Daskalov
Dimitar Daskalov

Reputation: 73


this has a solution, but onChange doesn't register the event. It won't change the state. Do not use it!

My suggestion is to use onInput, but DO NOT use the state variable as innerHTML for the editable element.

Put some default value, the contentEditable will handle the change of the innerHTML, while the onInput will handle the change of the state from the value of the innerHTML.

If you want to be sure that innerHTML will not be empty you can add onBlur event, that checks if it is ==="" and will update it with the default value or something else, etc.

Upvotes: 3

Sakshi
Sakshi

Reputation: 1548

This is what you need to have:

<div onChange={onChange}  contentEditable>

instead of onInput it should be onChange

demo:https://codesandbox.io/s/react-editable-cell-forked-ey7o1?file=/src/App.js

Upvotes: -1

Related Questions