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.
you can use autosize for that
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";
}
}}
/>
)}