在React中用js实现textarea自适应,不用div自己模拟。
以下内容在
chrome 65.0.3325.181和React 16.3.2试得。(使用了React提供的DOM对象,后续版本若有更改可能导致无法使用)(就是说太菜没有完全弄懂(逃
scrollHeight和offsetHeight
首先,需要了解scrollHeight和offsetHeight。
以下内容所述的
height为数值,非字符串。js获取到的为字符串。
对于textarea,offsetHeight是textarea显示的内容高度 + padding + border。当box-sizing : border-box时,offsetHeight恒等于height;当box-sizing : content-box时,offsetHeight恒大于height。无法体现出内容高度的变化,不符合要求。
对于textarea,scrollHeight是内容长度(行高之和)再算上上下padding,当未产生滚动条时scrollHeight真正高度是textarea显示的内容(空白)的高度 + 上下padding。所以当box-sizing : border-box时,scrollHeight在未产生滚动条时会小于height(height多了上下的border);当box-sizing : content-box时,scrollHeight恒大于等于height。
所以我选择在box-sizing : border-box下用scrollHeight来判断输入内容是否超过显示的高度。(实际上在box-sizing : content-box下也是可以的,但讲起来麻烦,原理也相同)
原理
以下均在
box-sizing : border-box下讨论
onChange Event
在React中,受控组件更新输入内容时要用到的是onChange事件。该事件触发的函数可接收一个参数event,event.target即为对应的textarea,可获取到height和scrollHeight。
增长
使用scrollHeight作为判断主要是当产生滚动条时,scrollHeight才会超过height,此时可以知道需要增加height的值了。
能够判断的关键是,每次获取到的scrollHeight都是最新的。这是由于每次获取scrollHeight,都会强制重绘,即使未更新输入内容,也会对捕获的输入进行判断,进而更新scrollHeight。此时获取到的scrollHeight就是包含了最后输入的内容的长度,即重绘后的长度。也就可以在使用setState()更新输入内容前,判断是否需要增长。(强制重绘发生在实际DOM元素上还是React的virtual DOM,想不到测试的方法,欢迎指点)
然后是需要增加多少。由于scrollHeight是内容长度(行高之和)再算上padding,当我们的输出超过显示高度时,实际上只增加了 行数。所以因为存在黏贴,即增加 多行 的情况,所以height也只需要增加 一个行高 即可。(因为输入没有突然增加多行的情况)height设置为 scrollHeight + 上下的border 即可。
缩短
需要缩短时,scrollHeight是小于height的。但也可能是恰好不需要缩短的情况。
由于获取scrollHeight会导致重绘,则只需要不断把height减去 一个行高,直到重绘后的scrollHeight超过了height,可判断此时已经不能再缩短height,退出循环。但此时内容长度已经超出height,故还需再给height加上 一个行高,使内容完全显示。
以下为代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37const lineHeight = 15, // 默认情况下为15,若修改了line-height,则改成line-height的值
borderTop = 1, // 默认为1
borderBottom = 1 // 默认为1
class Textarea extends Component {
constructor() {
super()
this.state = {
value : '',
}
this.handleValueChange = this.handleValueChange.bind(this)
}
handleValueChange(e) {
let height = parseInt(getComputedStyle(e.target).height.slice(0, -2), 10)
if (e.target.scrollHeight > height) {
e.target.style.height = `${e.target.scrollHeight + borderTop + borderBottom}px`
} else {
while (height >= e.target.scrollHeight) {
e.target.style.height = `${height - lineHeight}px`
height -= lineHeight
}
e.target.style.height = `${height + lineHeight}px`
}
this.setState({value : e.target.value})
}
render() {
return (
<textarea value={this.state.value}
onChange={this.handleValueChange}
>
</textarea>
)
}
}
stylus代码(即CSS)1
2
3
4
5
6
7
8
9
10textarea{
box-sizing: border-box
min-width: 450px
max-width: 450px
padding: 2px 4px
line-height: 15px // 默认值
height: 21px // 15 + (2 + 2) + (1 + 1) 即:行高 + 上下padding + 上下border
resize: none
overflow: hidden // 防止重绘时还没更新height产生滚动条,造成滚动条闪烁
}
在普通js中的注意事项
如果不是在React中,同样的做法也可以实现,原理也相同。此时重绘可确定发生在DOM元素上。但使用的事件为oninput。经查,React的onChange事件包含了oninput事件。且在React中,onChange与onInput无异。