在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
无异。