GAミント至上主義

コストにうるさいWEBアプリ開発者。最近はPython, Vue.js, Kubernetesがメイン@株式会社ビズオーシャン。https://github.com/uyamazak/

Vue.jsで要素の高さを使って入力値バリデーションする

現在、近日公開予定(サービス名も未定)の履歴書作成アプリをVue.jsとFirebaseでつくっている。

2018/07/12追記 公開しました

rirekisho.yagish.jp

その中で志望動機欄のような自由記述のテキストエリアのバリデーションがなんとかできたのでメモ。

こんな感じの欄。文字数や改行で高さがオーバーしたら一段階文字サイズを小さくして、大丈夫ならそのまま、それでもはみ出るならエラーを出したかった
f:id:uyamazak:20180618160130p:plain

単純に文字数、行数だけでは制限できない

テキストエリアなので自由に改行できるので文字数が少なくてもオーバーする場合がある
行数も計算しようと思ったが、文字が多い時は一段階は文字が小さくなるようにもしたいし、いろんなCSSプロパティが絡んできて何行になるかはよくわからない。
またCSSで文字サイズや行の高さを変えるたびにプログラム側で数字の調整を行うのは大変。

欄の高さは履歴書という紙への出力という目的上、そう変わらないからここが一定を超えたらエラーを出すようにしてみる。

例(この場で打った擬似コード)として、表示している箇所は下記のようなコンポーネントにしてある。

<template>
<div>
  <pre ref="reason" v-model="data.reason"></pre>
</div>
</template>

入力は親のコンポーネントでtextareaでやってるけど普通なので省略。

refを使ってclientHeightを取得する

vue側からアクセスできるようにref属性reasonをふる。

jp.vuejs.org


そうすれば、this.$refs.reason.clientHeight でclientHeightを取得できるのでこれを使う。

developer.mozilla.org

clientHeight の代わり?にgetBoundingClientRect()を使っても要素の位置やサイズが取得でき、高さも取れる。

今回試す限り同じ数値だったのでとりあえずclientHeight でいく。

また今回はtextarea内の改行をそのまま表示できるようにpreタグを使ってる。CSSでやってもいい。

$nextTickを使って描画変更後に高さを取得する

文字サイズを小さくするclassをつけても、すぐには反映されないので、$nextTickで次フレームの処理に入れる。
$nextTickは入れ子にもできる。

あとコメントで。

<script>
// 省略
  data: function () {
    return {
      data:{
        reason:''
      },
      classObject: {
        reason: {},
      },
      // 高さをpxで
      textareaMaxHeights: {
        reason: 180
      },
      errorMessages: {
        reason: '',
      }
    }
  },
  methods: {
    adjustFontSize: function (refName) {
      // 文字サイズを小さくするclass用のオブジェクトを初期化
      this.classObject[refName] = {}
      // エラーメッセージをいれるもの。まずは空で初期化
      this.errorMessages[refName] = ''
      // まずclassなしで描画されたものを$nextTickで確認
      this.$nextTick(function () {
        if (!this.$refs[refName]) return
        const maxHeight = this.textareaMaxHeights[refName]
        // 高さがオーバーしてたらfont-size-sm classを付ける
        if (this.$refs[refName].clientHeight > maxHeight) {
          this.classObject[refName] = {
            'font-size-sm': true
          }
        } else {
        // オーバーしてなかったら取る
          this.classObject[refName] = {}
        }
       // 文字サイズが反映されたら再度確認してオーバーしてたらエラーメッセージを入れる
        this.$nextTick(function () {
          if (this.$refs[refName].clientHeight > maxHeight) {
            this.errorMessages[refName] = '入力欄をオーバーしてます'
          }
        })
      })
    }
  },
  // 入力欄をwatchして変更のたびに実行。lodashのdebounceで連続実行を制御してもいいかもしれない。
  watch: {
    'data.reason': function () {
      this.adjustFontSize('reason')
    }
  }
}
</script>

これで入力画面からはerrorMessagesを見てエラーの出し分けなどをする。複雑になってきたらvuexとか使ったほうがいいかも。

$nextTickの使い方で試行錯誤しまくったのでまだバグがありそう。

ちょっとはまったのが、clientHeight がv-if,v-showで隠してしまっていると取れないこと。
v-ifでfalseだとそもそも要素がなく、v-showだとdisplay:noneで0になってしまう。

今回入力画面と、表示確認画面の表示を切り替えるUIだったので同時に表示ができなかった。

そのため表示する場所を隠したい場合は下記CSSで隠した。

visibility: hidden;
overflow: hidden;
height: 0;