javascript/react dynamic height textarea (stop at a max)

What I’m trying to achieve is a textarea that starts out as a single line but will grow up to 4 lines and at that point start to scroll if the user continues to type. I have a partial solution kinda working, it grows and then stops when it hits the max, but if you delete text it doesn’t shrink like I want it to.

This is what I have so far.

export class foo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      textareaHeight: 38
    };
  }

  handleKeyUp(evt) {
    // Max: 75px Min: 38px
    let newHeight = Math.max(Math.min(evt.target.scrollHeight + 2, 75), 38);
    if (newHeight !== this.state.textareaHeight) {
      this.setState({
        textareaHeight: newHeight
      });
    }
  }

  render() {
    let textareaStyle = { height: this.state.textareaHeight };
    return (
      <div>
        <textarea onKeyUp={this.handleKeyUp.bind(this)} style={textareaStyle}/>
      </div>
    );
  }
}

Obviously the problem is scrollHeight doesn’t shrink back down when height is set to something larger. Any suggestion for how I might be able to fix this so it will also shrink back down if text is deleted?

ANOTHER SIMPLE APPROACH (without an additional package)

export class foo extends React.Component {
  handleKeyDown(e) {
    e.target.style.height="inherit";
    e.target.style.height = `${e.target.scrollHeight}px`; 
    // In case you have a limitation
    // e.target.style.height = `${Math.min(e.target.scrollHeight, limit)}px`;
  }

  render() {
    return <textarea onKeyDown={this.handleKeyDown} />;
  }
}

The problem when you delete the text and textarea doesn’t shrink back is because you forget to set this line

e.target.style.height="inherit";

Consider using onKeyDown because it works for all keys while others may not (w3schools)

In case you have padding or border of top or bottom. (reference)

handleKeyDown(e) {
    // Reset field height
    e.target.style.height="inherit";

    // Get the computed styles for the element
    const computed = window.getComputedStyle(e.target);

    // Calculate the height
    const height = parseInt(computed.getPropertyValue('border-top-width'), 10)
                 + parseInt(computed.getPropertyValue('padding-top'), 10)
                 + e.target.scrollHeight
                 + parseInt(computed.getPropertyValue('padding-bottom'), 10)
                 + parseInt(computed.getPropertyValue('border-bottom-width'), 10);

    e.target.style.height = `${height}px`;
}

I hope this may help.

Read More:   Websocket frame size limitation

you can use autosize for that

LIVE DEMO

import React, { Component } from 'react';
import autosize from 'autosize';

class App extends Component {
    componentDidMount(){
       this.textarea.focus();
       autosize(this.textarea);
    }
    render(){
      const style = {
                maxHeight:'75px',
                minHeight:'38px',
                  resize:'none',
                  padding:'9px',
                  boxSizing:'border-box',
                  fontSize:'15px'};
        return (
          <div>Textarea autosize <br/><br/>
            <textarea
            style={style} 
            ref={c=>this.textarea=c}
            placeholder="type some text"
            rows={1} defaultValue=""/>
          </div>
        );
    }
}

or if you prefer react modules https://github.com/andreypopp/react-textarea-autosize

Just use useEffect hook which will pick up the height during the renderer:

import React, { useEffect, useRef, useState} from "react";
const defaultStyle = {
    display: "block",
    overflow: "hidden",
    resize: "none",
    width: "100%",
    backgroundColor: "mediumSpringGreen"
};

const AutoHeightTextarea = ({ style = defaultStyle, ...etc }) => {
    const textareaRef = useRef(null);
    const [currentValue, setCurrentValue ] = useState("");// you can manage data with it

    useEffect(() => {
        textareaRef.current.style.height = "0px";
        const scrollHeight = textareaRef.current.scrollHeight;
        textareaRef.current.style.height = scrollHeight + "px";
    }, [currentValue]);

    return (
        <textarea
            ref={textareaRef}
            style={style}
            {...etc}
            value={currentValue}

            onChange={e=>{
            setCurrentValue(e.target.value);
            //to do something with value, maybe callback?
            }}
        />
    );
};

export default AutoHeightTextarea;

Really simple if you use hooks “useRef()”.

css:

.text-area {
   resize: none;
   overflow: hidden;
   min-height: 30px;
}

react componet:

export default () => {
 const textRef = useRef<any>();

 const onChangeHandler = function(e: SyntheticEvent) {
  const target = e.target as HTMLTextAreaElement;
  textRef.current.style.height = "30px";
  textRef.current.style.height = `${target.scrollHeight}px`;
 };

 return (
   <div>
    <textarea
      ref={textRef}
      onChange={onChangeHandler}
      className="text-area"
     />
    </div>
  );
};

you can even do it with react refs. as setting ref to element

<textarea ref={this.textAreaRef}></textarea> // after react 16.3
<textarea ref={textAreaRef=>this.textAreaRef = textAreaRef}></textarea> // before react 16.3

and update the height on componentDidMount or componentDidUpdate as your need. with,

if (this.textAreaRef) this.textAreaRef.style.height = this.textAreaRef.scrollHeight + "px";

actually you can get out of this with useState and useEffect

function CustomTextarea({minRows}) {
  const [rows, setRows] = React.useState(minRows);
  const [value, setValue] = React.useState("");
  
  React.useEffect(() => {
    const rowlen = value.split("\n");

    if (rowlen.length > minRows) {
      setRows(rowlen.length);
    }
  }, [value]);

  return (
    <textarea rows={rows} onChange={(text) => setValue(text.target.value)} />
  );
}

Uses

<CustomTextarea minRows={10} />

I like using this.yourRef.current.offsetHeight. Since this is a textarea, it wont respond to height:min-content like a <div style={{height:"min-content"}}>{this.state.message}</div> would. Therefore I don’t use

uponResize = () => {
 clearTimeout(this.timeout);
  this.timeout = setTimeout(
   this.getHeightOfText.current &&
   this.setState({
    heightOfText: this.getHeightOfText.current.offsetHeight
   }),
  20
 );
};
componentDidMount = () => {
 window.addEventListener('resize', this.uponResize, /*true*/)
}
componentWillUnmount = () => {
 window.removeEventListener('resize', this.uponResize)
}

but instead use

componentDidUpdate = () => {
 if(this.state.lastMessage!==this.state.message){
  this.setState({
   lastMessage:this.state.message,
   height:this.yourRef.current.offsetHeight
  })
 }
}

on a hidden div

<div
 ref={this.yourRef}
 style={{
  height:this.state.height,
  width:"100%",
  opacity:0,
  zIndex:-1,
  whiteSpace: "pre-line"
 })
>
 {this.state.message}
</div>

Using hooks + typescript :

import { useEffect, useRef } from 'react';
import type { DetailedHTMLProps, TextareaHTMLAttributes } from 'react';

// inspired from : https://stackoverflow.com/a/5346855/14223224
export const AutogrowTextarea = (props: DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>) => {
  const ref = useRef<HTMLTextAreaElement>(null);
  let topPadding = 0;
  let bottomPadding = 0;

  const resize = () => {
    ref.current.style.height="auto";
    ref.current.style.height = ref.current.scrollHeight - topPadding - bottomPadding + 'px';
  };

  const delayedResize = () => {
    window.setTimeout(resize, 0);
  };

  const getPropertyValue = (it: string) => {
    return Number.parseFloat(window.getComputedStyle(ref.current).getPropertyValue(it));
  };

  useEffect(() => {
    [topPadding, bottomPadding] = ['padding-top', 'padding-bottom'].map(getPropertyValue);

    ref.current.focus();
    ref.current.select();
    resize();
  }, []);

  return <textarea ref={ref} onChange={resize} onCut={delayedResize} onPaste={delayedResize} onDrop={delayedResize} onKeyDown={delayedResize} rows={1} {...props} />;
};

import { useRef, useState } from "react"
const TextAreaComponent = () => {
  const [inputVal, setInputVal] =useState("")
  const inputRef = useRef(null)



  const handleInputHeight = () => {
const scrollHeight = inputRef.current.scrollHeight;
inputRef.current.style.height = scrollHeight + "px";
 };

 const handleInputChange = () => {
  setInputVal(inputRef.current.value)
  handleInputHeight()
 }
 
 return (
    <textarea 
     ref={inputRef}  
     value={inputVal}
     onChange={handleInputChange} 
      onKeyDown={(e) => {
          if (e.key === "Enter") {
            handleSubmit(e);
            inputRef.current.style.height = "40px";
          }
        }}
     />
  )}


The answers/resolutions are collected from stackoverflow, are licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0 .

Similar Posts