Note: This is not a duplicate as far as I can tell, as using a contentEditable
div doesn't seem to be a good alternative. It has numerous problems (no placeholder text, need to use the dangerouslySetInnerHTML
hack to update text, selection cursor is finicky, other browser issues, etc.) I would like to use a textarea.
I'm currently doing something this for my React textarea component:
componentDidUpdate() {
let target = this.textBoxRef.current;
target.style.height = 'inherit';
target.style.height = `${target.scrollHeight + 1}px`;
}
This works and allows the textarea to dynamically grow and shrink in height as line breaks are added and removed.
The problem is that on every text change there is a reflow occurring. This causes a lot of lag in the application. If I hold down a key in the textarea there is delay and lag as the characters are appended.
If I remove the target.style.height = 'inherit';
line the lag goes away, so I know it's being caused by this constant reflow.
I heard that setting overflow-y: hidden
might get rid of the constant reflow, but it did not in my case. Likewise, setting target.style.height = 'auto';
did not allow for dynamic resize.
I currently have developed a solution to this which works, but I don't like it, as it is an O(n) operation for every time the text changes. I just count the number of line breaks and set the size accordingly, like this:
// In a React Component
handleMessageChange = e => {
let breakCount = e.target.value.split("\n").length - 1;
this.setState({ breakCount: breakCount });
}
render() {
let style = { height: (41 + (this.state.breakCount * 21)) + "px" };
return (
<textarea onChange={this.handleMessageChange} style={style}></textarea>
);
}
I think thirtydot's recommendation may be the best. The Material UI textarea he linked has a pretty clever solution.
They create a hidden absolutely positioned textarea that mimics the style and width of the actual textarea. Then they insert the text you type into that textarea and retrieve the height of it. Because it is absolutely positioned there is no reflow calculation. They then use that height for the height of the actual textarea.
I don't fully understand all of what their code is doing, but I've hacked together a minimal repurposement for my needs, and it seems to work well enough. Here are some snippets:
.shadow-textarea {
visibility: hidden;
position: absolute;
overflow: hidden;
height: 0;
top: 0;
left: 0
}
<textarea ref={this.chatTextBoxRef} style={{ height: this.state.heightInPx + "px" }}
onChange={this.handleMessageChange} value={this.props.value}>
</textarea>
<textarea ref={this.shadowTextBoxRef} className="shadow-textarea" />
componentDidUpdate() {
this.autoSize();
}
componentDidMount() {
this.autoSize();
}
autoSize = () => {
let computedStyle = window.getComputedStyle(this.chatTextBoxRef.current); // this is fine apparently..?
this.shadowTextBoxRef.current.style.width = computedStyle.width; // apparently width retrievals are fine
this.shadowTextBoxRef.current.value = this.chatTextBoxRef.current.value || 'x';
let innerHeight = this.shadowTextBoxRef.current.scrollHeight; // avoiding reflow because we are retrieving the height from the absolutely positioned shadow clone
if (this.state.heightInPx !== innerHeight) { // avoids infinite recursive loop
this.setState({ heightInPx: innerHeight });
}
}
A bit hacky but it seems to work well enough. If anyone can decently improve this or clean it up with a more elegant approach I'll accept their answer instead. But this seems to be the best approach considering Material UI uses it and it is the only one I've tried so far that eliminates the expensive reflow calculations that cause lag in a sufficiently complex application.
Chrome is only reporting reflow occurring once when the height changes, as opposed to on every keypress. So there is still a single 30ms lag when the textarea grows or shrinks, but this is much better than on every key stroke or text change. The lag is 99% gone with this approach.
NOTE: Ryan Peschel's answer is better.
Original Post: I have heavily modified apachuilo's code to achieve the desired result. It adjusts the height based on the scrollHeight
of the textarea
. When the text in the box is changed, it sets the box's number of rows to the value of minRows
and measures the scrollHeight
. Then, it calculates the number of rows of text and changes the textarea
's rows
attribute to match the number of rows. The box does not "flash" while calculating.
render()
is only called once, and only the rows
attribute is changed.
It took about 500ms to add a character when I put in 1000000 (a million) lines of at least 1 character each. Tested it in Chrome 77.
CodeSandbox: https://codesandbox.io/s/great-cherry-x1zrz
import React, { Component } from "react";
class TextBox extends Component {
textLineHeight = 19;
minRows = 3;
style = {
minHeight: this.textLineHeight * this.minRows + "px",
resize: "none",
lineHeight: this.textLineHeight + "px",
overflow: "hidden"
};
update = e => {
e.target.rows = 0;
e.target.rows = ~~(e.target.scrollHeight / this.textLineHeight);
};
render() {
return (
<textarea rows={this.minRows} onChange={this.update} style={this.style} />
);
}
}
export default TextBox;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With