Bootstrapを利用してフォームを実装しましたため、v-modelやバリデートなどについてまとめます。

イケてるフォーム


初期状態です。
入力内容に不備がある入力欄はエラーとして赤字、赤枠で表示します。
また全ての不備が取り切れるまでSubmitボタンはdisabledとします。


何か入力し、エラーがなくなるとリアルタイムでフォームに反映されます。


全てのエラーがなくなると、Submitボタンがアクティブになり、ボタンを押下することができます。


ごめんなさい、普通のフォームでした。

環境

  • Node.js 7.7.x
  • npm 4.4.x
  • Vue.js 2.2.x

素のHTML


上の状態、Vue.jsの処理を入れる前の状態のHTMLです。

<form class="form-horizontal">
  <fieldset>
    <legend>書籍登録</legend>
    <div class="form-group">
      <label for="inputTitle" class="col-md-2 control-label">タイトル</label>
      <div class="col-md-10">
        <input type="text" class="form-control" id="inputTitle" placeholder="書籍タイトル">
      </div>
    </div>
    <div class="form-group">
      <label for="inputSummary" class="col-md-2 control-label">サマリ</label>
      <div class="col-md-10">
        <textarea class="form-control" rows="3" id="inputSummary"></textarea>
      </div>
    </div>
    <div class="form-group">
      <label for="inputIsbn" class="col-md-2 control-label">ISBN</label>
      <div class="col-md-10">
        <input type="text" class="form-control" id="inputIsbn" placeholder="ISBN">
      </div>
    </div>
    <div class="form-group">
      <label for="inputRelease" class="col-md-2 control-label">発売日</label>
      <div class="col-md-10">
        <input type="date" class="form-control" id="inputRelease">
      </div>
    </div>
    <div class="form-group">
      <div class="col-md-10 col-md-offset-2">
        <button type="submit" class="btn btn-primary center-block">Submit</button>
      </div>
    </div>
  </fieldset>
</form>

INPUTタグのname属性は利用しないため、省略しております。

Vue.jsの組み込み

ソース全文

<template>
  <form class="form-horizontal">
    <fieldset>
      <legend>書籍登録</legend>
      <div :class="errorClassObject('title')" class="form-group">
        <label for="inputTitle" class="col-md-2 control-label">タイトル</label>
        <div class="col-md-10">
          <input v-model="edit.title" type="text" class="form-control" id="inputTitle" placeholder="書籍タイトル">
        </div>
      </div>
      <div :class="errorClassObject('summary')" class="form-group">
        <label for="inputSummary" class="col-md-2 control-label">サマリ</label>
        <div class="col-md-10">
          <textarea v-model="edit.summary" class="form-control" rows="3" id="inputSummary"></textarea>
        </div>
      </div>
      <div :class="errorClassObject('isbn')" class="form-group">
        <label for="inputIsbn" class="col-md-2 control-label">ISBN</label>
        <div class="col-md-10">
          <input v-model="edit.isbn" type="text" class="form-control" id="inputIsbn" placeholder="ISBN">
        </div>
      </div>
      <div :class="errorClassObject('release')" class="form-group">
        <label for="inputRelease" class="col-md-2 control-label">発売日</label>
        <div class="col-md-10">
          <input v-model="edit.release" type="date" class="form-control" id="inputRelease">
        </div>
      </div>
      <div class="form-group">
        <div class="col-md-10 col-md-offset-2">
          <button
            @click="doSubmit"
            :disabled="isValid == false"
            type="submit" class="btn btn-primary center-block">Submit</button>
        </div>
      </div>
    </fieldset>
  </form>
</template>
<script>
const isbn10RE = /^[0-9]{9}[0-9X]$/
const dateRE   = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/

export default {
  data() {
    return {
      edit: {
        title  : "",
        summary: "",
        isbn   : "",
        release: ""
      },
      maxLength: 10
    }
  },
  computed: {
    validation() {
      const edit = this.edit
      return {
        title  : (!!edit.title && edit.title.length <= this.maxLength),
        summary: (!!edit.summary),
        isbn   : (!!edit.isbn    && isbn10RE.test(edit.isbn)),
        release: (!!edit.release && dateRE.test(edit.release))
      }
    },
    isValid() {
      const validation = this.validation
      return Object
        .keys(validation)
        .every(function (key) {
          return validation[key]
       })
     }
  },
  methods: {
    errorClassObject(key) {
      return {
        'has-error': (this.validation[key] == false)
      }
    },
    doSubmit() {
      // 美しさに感動してしまうPOST処理
    }
  }
}
</script>

解説

処理の流れ

コードの動かすと次のような流れで処理が連携されていることがわかります。

  • v-modelを設定したリアクティブなINPUTタグを入力する
  • 算出プロパティが再計算され、各パラメーターのバリデート結果が更新される
  • バリデート結果に応じてhas-errorクラスのバインドも更新される

それぞれ見ていきましょう。

v-model

Vue.js > ガイド > フォーム入力バインディング
https://jp.vuejs.org/v2/guide/forms.html

フォームといえばv-modelです。

v-modelをHTMLに埋め込むだけで簡単に双方向データバインディングを実現することができます。

<input v-model="edit.title" type="text" class="form-control" id="inputTitle" placeholder="書籍タイトル">

たったこれだけです。
あまりにも簡単なため、説明することがありません。。。

クラスのバインディング

Vue.js > ガイド > クラスとスタイルのバインディング
https://jp.vuejs.org/v2/guide/class-and-style.html

バリデートエラー時にクラス[ has-error ]を適用するため、v-bindでクラスをバインドします。

基本のフォーマットは次です。

v-bind:class="{クラス名: 式}"

上がオブジェクト構文ですね。

今回のHTMLをオブジェクト構文で記述するならば次のようになります。

v-bind:class="{'has-error': validation.title == false }"

私は視認性から否定( ! )をあまり好まないため、[ == false]を使うようにしているのですが、全てのパラメーターにこれを埋め込むのは手間であるため、メソッドをバインドしております。

errorClassObject(key) {
  return {
    'has-error': (this.validation[key] == false)
  }
}

噛み砕くと、「クラス名と、そのクラスを適用するかの真理値」を対にしたオブジェクトを与えれば良いのです。

たとえば、次のようにして複数のクラスを適用することも可能です。

{
  'class-a': true,
  'class-b': true
}

Vue.jsでは複雑な処理はテンプレート書かず、メソッドなり、後述する算出プロパティに実装することが推奨されています。

できるだけHTMLはテンプレートを、JSはscriptに実装することが見通しの良いコードにするための第一歩ですよね。

バリデーション

Vue.js > ガイド > 算出プロパティとウォッチャ
https://jp.vuejs.org/v2/guide/computed.html

今回のサンプルでは算出プロパティを用いてバリデーション結果を管理しております。

算出プロパティはキャッシュされ、リアクティブ依存が変更されたときだけ再計算されることがの大きな特徴です。
名称から常に計算されるコストが高い処理と誤解してしまいましたが、そうではありませんでした。

サンプルの該当部分は以下です。

computed: {
 validation() {
   const edit = this.edit
   return {
     title  : (!!edit.title),
     :
   }
 },
 isValid() {
   const validation = this.validation
   return Object
     .keys(validation)
     .every(function (key) {
       return validation[key]
    })
  }
},

validation()は、this.editの各値をチェックし、次の形のオブジェクトを返しています。

{
  'title': true,
  'summary': false,
  :
}

isValid()はvalidation()の結果を受け、全てのバリデーションがtrueかをチェックする関数です。

これを利用し、Submitボタンにprop.disabledをバインドしています。

<button :disabled="isValid == false" ,,,

validation()、isValid()は、ほぼ次の公式ページのままです。

Vue.js > 例 > Firebase + バリデーション の例
https://jp.vuejs.org/v2/examples/firebase.html

もう一歩

ライブラリ連携

既存コードがBootstraでフォームを組み立てている場合など、日付入力にBootstrap datepickerを利用している場合があると思います。

このdatepikerを利用する場合、例えば、ブラウザ上ではdatepickerで「1/17」を「1/31」に変更しても、Vue.jsのコードでは編集前の「1/17」となってしまうようなど、そのままでは「画面表示とスクリプトから見た値が異なる」という不具合 となってしまいます。

このようになってしまう理由はdatepickerがユーザ入力を伴わずにDOMを更新してしまい、Vue.js側で変更を検知できないためのようです。

この対処方法は「datepicker.onでイベントを購読する」で、Githubにissueが上がっていました。

when use vue2.0 and bootstrap-datepicker, and use v-model can not get the date value. #4231
https://github.com/vuejs/vue/issues/4231

上記からコードを引用します。
次のようにon.changeDateを設定して、Vueのdataに直接セットしてあげます。

$('#startDate').datepicker().on(
  'changeDate',() => {
    this.startDate = $('#startDate').val()
  }
)

仕方がありませんが、HACKですね。


なお、次のようなVue.js用のdatepiekerも公開されております。

hilongjw/vue-datepicker
https://github.com/hilongjw/vue-datepicker

複雑なバリデーション

複雑なバリデーションを行う場合、メソッドを定義したくなりますが、書き方に注意してください。

次の書き方は期待通りにはなりません。

validation() {
  return {
    title: () => {
      // これはNG、なぜなら関数が定義されるだけだから
    },
  }
},

このように即時関数を利用するとOKでした。

validation() {
  return {
    title: (() => {
      if (hoge) {
        return false
      }
      return true
    })()
  }
},

vue-validator

複雑なバリデートを行う場合、vue-validatorという素晴らしいライブラリが公開されております。

kazupon/vue-validator
https://github.com/kazupon/vue-validator

複雑な構造

イテレーターを利用する場合など、validation()で返すオブジェクトの階層が深くなる場合もあると思います。

validation() {
  return {
    address1: {
      zipCode: true,
      phone  : true,
    },
    address2: {
      zipCode: true,
      phone  : true,
    }
  }
},

前述のisValid()はオブジェクトが1階層であることを前提にしておりそのままでは動きません。
そのため、次のようにオブジェクトのチェックを切り出すなどして対処すればOKです。

computed: {
  isValid() {
    const validation = this.validation
    return (
      this.itOkey(validation.address1) &&
      this.itOkey(validation.address2)
    )
  },
},
methods: {
  itOkey(params)
  {
    return Object
      .keys(params)
      .every(function (key) {
        return params[key]
    })
  }
}

おわりに

Vue.jsならフォームに双方向バインディングするのも、バリデーション結果をリアルタイムに反映するのも驚くほど簡単です。
この記事が、モダンJS導入のとっかかりになりますと幸いです。

この記事はtomita@atuwebがお届けしました。



Learning Vue.js 2