textareaの高さを自動調整する方法 Vue.js編

HTMLのtextareaの高さはrows属性によって行を設定することはできますが、
一度設定すると変更を行うことができません。本文書ではvue.jsを利用してユーザが入力した行によってtextareaの高さを自動拡張、自動収縮する機能を作成します。
textareaの高さの自動調整についてはvue-textarea-autosize-masterというパッケージがあるので中身を参考にさせてもらいました。
手元の環境で動作確認できるようにcdnを利用して作成しています。

初期設定
まずvue.jsが動作することを確認するためにブラウザ上にHelloを表示させます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>Vue</title>
</head>
<body>
<div id="app">
<h1>{{ tweet }}</h1>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
<script>
new Vue({
el: "#app",
data() {
return {
tweet: "Hello",
};
},
})
</script>
</body>
</html>
画面にHelloが表示されるのを確認します。

textareaの設定
textareaタグを追加して、v-modelでtweetを設定します。textareaの高さはstyleタグを利用して行うのでvue側からstyleを制御できるようにバインディングを行います。
<textarea v-model="tweet" :style="styles"></textarea>
バインドのために設定を行ったstylesはcomputedプロパティを使ってvueに追加します。高さの初期値は60pxに設定しておきます。つまりcomputedプロパティを通してheightに60pxを設定していることになります。
new Vue({
el: "#app",
data() {
return {
tweet: "Hello",
height:"60px",
};
},
computed:{
styles(){
return {
"height": this.height,
}
}
},
})
ブラウザで確認すると高さが60pxのtextareaが表示されます。

textareの高さを調べる方法
textareaを自動調整するためにはtextareaの高さが知っておく必要があります。高さを取得するためにscrollHeightを利用します。textareaのscrollHeightを知るためにはtextareaの要素を取得する必要があります。vue.jsではrefを利用することで直接要素にアクセスを行なって情報を取得することができます。ここでもrefを利用してtextareaの要素を取得します。
textareaにref属性を追加し識別するための任意の名前を設定します。ここではareaという名前をつけています。refを設定した要素はvue側から直接アクセスすることが可能となります。
<textarea v-model="tweet" ref="area" :style="styles"></textarea>
vue側ではthis.$refsに先程textareaに設定したareaを使いthis.$refs.areaでrefを設定したtextareaの要素を取得することができます。本当にここまでの設定で要素の情報が取得できているのか確認するためにライフサイクルフックmountedで要素が取得できていることを確認してみましょう。
mounted(){
console.log(this.$refs.area);
}
ブラウザのデベロパーツールのコンソールを確認すると下記が表示されます。

高さはscrollHeightを追加することで取得できます。64と表示されます。
mounted(){
console.log(this.$refs.area.scrollHeight);
}
高さを取得するタイミング
高さはtextareaに文字を入力・削除した時に取得したいためvueのwatchを利用します。入力した文字を保存するデータプロパティtweetを監視し文字の入力・削除を検知します。
watch:{
tweet(){
console.log(this.$refs.area.scrollHeight);
},
},
上記の設定が完了したら文字の入力を行ってください。文字を入力する度にコンソールにメッセージが表示されます。
3行目まで文字を入力しても64のメッセージが表示されますが、4行目に入るとメッセージに表示される値が76になることを確認してください。初期のtextareaに入りきらないため高さが増えたことを表しています。textareaの右側にはスクロールバーが表示されます。

さらに行を進めていくとメッセージに表示される数字は行の数に比例して大きくなります。今後は行を削除してください。増えた数字が減少することになります。
この数字を監視してcomputedプロパティを通して設定することができれば、行に合わせてtextareaの領域を自動で変更することができるというイメージが湧くかと思います。
resizeメソッドの追加
次に高さを設定するresizeメソッドを作成していきます。下記のコードの中で”この行だけでOK?”とコメントを付けましたが、通常であればこの1行であれば動作するように思うかもしれません。しかし、この行だけでは正常に動作しません。この1行を設定すると文字を入力するたびにコンソールに出力したheightの値が4ずつ増えます。この問題を解消するため以下の2点を設定する必要があります。
1点目はnexTickを設定する必要があります。nextTickを利用することでDOMの更新が完了する前のscrollHeightの値を取得することを避けることができます。nextTickを使う場合は、引数のcallback関数の中でscrollHeightの取得の処理を行います。
2点目は、nextTickの処理の前にthis.heightの値を設定する必要があります。ここではautoに設定しています。
この2点を設定することでresize関数は先ほどの問題を解消することができます。
methods:{
resize(){
this.height = this.$refs.area.scrollHeight + 'px';//この行だけでOK?
this.height = "auto";
this.$nextTick(()=>{
this.height = this.$refs.area.scrollHeight + 'px';
})
}
},

最後に追加したresizeメソッドをmounted時とwatchの中に登録します。mount時にresizeを実行するためtextareaの高さはresize関数の中のthis.heightの高さに設定されます。
new Vue({
el: "#app",
data() {
return {
tweet: "Hello",
height:"60px",
};
},
computed:{
styles(){
return {
"height": this.height,
}
}
},
watch:{
tweet(){
this.resize();
},
},
methods:{
resize(){
this.height = "auto";
this.$nextTick(()=>{
this.height = this.$refs.area.scrollHeight + 'px';
})
}
},
mounted(){
this.resize();
}
})
最初は下記のようにHelloが表示されます。

文字を入力すると行の高さに合わせてtextaregの高さが増えます。

今度は増えた状態から行を削除すると高さが減ります。

このようにvue.jsを使うことでtextareaの高さを自動調整できるコードを作成することができます。作成したコードは下記の通りです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>Vue</title>
</head>
<body>
<div id="app">
<h1>{{ tweet }}</h1>
<textarea v-model="tweet" ref="area" :style="styles"></textarea>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
<script>
new Vue({
el: "#app",
data() {
return {
tweet: "Hello",
height:"60px",
};
},
computed:{
styles(){
return {
"height": this.height,
}
}
},
watch:{
tweet(){
this.resize();
},
},
methods:{
resize(){
this.height = "auto";
this.$nextTick(()=>{
this.height = this.$refs.area.scrollHeight + 'px';
})
}
},
mounted(){
this.resize();
}
})
</script>
</body>
</html>