Flosrn
Flosrn

Reputation: 95

How to dynamically adjust textarea height with React?

I want to adjust my textarea height dynamically with Refs and pass it to the state but it don't work correctly.

I created a codesandbox to help you to understand what exactly I want.

https://codesandbox.io/s/ol5277rr25

Upvotes: 5

Views: 20085

Answers (4)

user28093031
user28093031

Reputation: 1

interface IProps {
  value: string | number;
  onChange: (value: string) => void;
  cellId: string;
  onKeyDownForNumberInput?: (e: KeyboardEvent) => void;
  handleBlur: () => void;
  isReadOnly: boolean;
}

const TableCellEditable = forwardRef(
  (
    {
      value,
      onChange,
      isReadOnly,
      cellId,
      onKeyDownForNumberInput,
      handleBlur,
    }: IProps,
    ref: any
  ): JSX.Element => {
    const [position, setPosition] = useState<number>(0);
    // Вариант из stackOverflow
    const getCursorPosition = (parent: Node): void => {
      let selection = document?.getSelection();
      let range = new Range();
      range?.setStart(parent, 0);
      if (
        selection?.anchorNode !== null &&
        selection?.anchorNode !== undefined &&
        selection?.anchorOffset !== null &&
        selection?.anchorOffset !== undefined
      ) {
        range.setEnd(selection?.anchorNode, selection?.anchorOffset);
        const position = range?.toString()?.length;
        setPosition(position);
      }
    };
    // Вариант из stackOverflow
    const setCursorPosition = (parent: Node, position: number): void => {
      let child = parent?.firstChild;
      if (child) {
        while (position > 0) {
          let length = child?.textContent?.length;
          if (length !== undefined && position > length) {
            position -= length;
            child = child?.nextSibling as any;
          } else {
            if (child?.nodeType === 3)
              return document?.getSelection()?.collapse(child, position);
            child = child?.firstChild as any;
          }
        }
      }
    };

    const handleChange = (evt: React.ChangeEvent<any>) => {
      let el = document?.getElementById(cellId);
      if (el) {
        getCursorPosition(el);
      }
      onChange(evt.target.textContent);
    };

    const onBlur = () => {
      handleBlur();
    };

    useLayoutEffect(() => {
      let el = document?.getElementById(cellId);
      if (el) {
        setCursorPosition(el, position);
      }
    }, [value, cellId, position]);

    return (
      <table className={cx("table")}>
        <tbody>
          <tr className={cx("table__row")}>
            {!isReadOnly ? (
              <td
                suppressContentEditableWarning={true}
                contentEditable={true}
                onInput={handleChange}
                ref={ref as any}
                id={cellId}
                className={cx("table__cell")}
                onKeyDown={onKeyDownForNumberInput as any}
                onBlur={onBlur}
              >
                {value}
              </td>
            ) : (
              <td className={cx("readonly")}>{value}</td>
            )}
          </tr>
        </tbody>
      </table>
    );

I was faced with inserting a text area into the <td> cell, and it increased as I entered the text. I tried it in different ways. As a result, I made the cell itself

<td suppressContentEditableWarning={true}
contentEditable={true}>

. The React showed an error without the suppressContentEditableWarning={true} property. But it works in Chrome and Firefox as needed. Also, the onChange event does not work. Used the onInput={handleInputChange} event. And since there is no value in <td>, I took event.target.textContent. I don't know how safe and correct this is. Therefore, you do all this under your responsibility.

Update. I'm sorry, I'm new to posting here. In general, the method I described only works a little. Since react works unpredictably with contentEditable, the cursor is lost when entering text!!! (You can make the component memoized with a ban on updating, but this leads to unpredictable work) Now it seems that I have solved the problem with the help of the library https://www.npmjs.com/package/react-contenteditable . It allows you to work in react applications with tags with the contentEditable property. So what I did: instead of a cell, I inserted a component provided by the library into my table, and this cell became an editable cell that automatically increases its height and width if necessary. you can use your own tag. I have not checked carefully, so the application of this approach is your responsibility

UPD: working code in snippet. With the correct carriage behavior. The code that is responsible for the carriage is not mine, I took it from the Internet

Upvotes: 0

RandomDude
RandomDude

Reputation: 1141

You can solve this by using useRef and useLayoutEffect built-in hooks of react. This approach updates the height of the textarea before any rendering in the browser and therefor avoids any "visual update"/flickering/jumping of the textarea.

import React from "react";

const MIN_TEXTAREA_HEIGHT = 32;

export default function App() {
  const textareaRef = React.useRef(null);
  const [value, setValue] = React.useState("");
  const onChange = (event) => setValue(event.target.value);

  React.useLayoutEffect(() => {
    // Reset height - important to shrink on delete
    textareaRef.current.style.height = "inherit";
    // Set height
    textareaRef.current.style.height = `${Math.max(
      textareaRef.current.scrollHeight,
      MIN_TEXTAREA_HEIGHT
    )}px`;
  }, [value]);

  return (
    <textarea
      onChange={onChange}
      ref={textareaRef}
      style={{
        minHeight: MIN_TEXTAREA_HEIGHT,
        resize: "none"
      }}
      value={value}
    />
  );
}

https://codesandbox.io/s/react-textarea-auto-height-s96b2

Upvotes: 14

user3006381
user3006381

Reputation: 2885

Here's a simple solution that doesn't involve refs. The textarea is dynamically adusted using some CSS and the rows attribute. I used this myself, recently (example: https://codesandbox.io/embed/q8174ky809).

In your component, grab the textarea, calculate the current number of rows, and add 1:

const textArea = document.querySelector('textarea')
const textRowCount = textArea ? textArea.value.split("\n").length : 0
const rows = textRowCount + 1

return (
  <div>
    <textarea
      rows={rows}
      placeholder="Enter text here."
      onKeyPress={/* do something that results in rendering */}
      ... />
  </div>
)

And in your CSS:

textarea {
  min-height: 26vh; // adjust this as you see fit
  height: unset; // so the height of the textarea isn't overruled by something else
}

Upvotes: 7

Mustafa
Mustafa

Reputation: 158

You can check the repo. Or you can add the package to your project.

https://github.com/andreypopp/react-textarea-autosize

Also if you really willing to learn how the logic working exactly;

https://github.com/andreypopp/react-textarea-autosize/blob/master/src/calculateNodeHeight.js

There is a source code with all calculations together.

Upvotes: 0

Related Questions