Vue2 & Vuetify2でバリデーション付きのフォームを作ってみる

:::note warn
この記事は、Vue2.x 版です。Vue3.x での内容をご覧になりたい方は、「[Vue3 & Vuetify3でバリデーション付きのフォームを作ってみる](https://qiita.com/tekunikaruza_jp/items/116dc365af3d442ed9ec)」の記事をご覧ください。
:::

[Vue](https://jp.vuejs.org/index.html)と[Vuetify](https://vuetifyjs.com/ja/)を組み合わせて、下図キャプチャのようなバリデーション付きのフォームを作ってみます。Vueの基本事項は知っている前提です。だいぶ回りくどい記事になっています。

![完成例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84550/20d7db04-0baa-bac4-7d86-dcab4ac4fc1a.gif)

# バージョン

* Vue 2.6.10
* Vuetify 2.1.1

# STEP0: ベース

## デモ

<p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="html,result" data-user="tekunikaruza" data-slug-hash="NWWWyea" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Vue &amp;amp; Vuetifyでのバリデーション付きフォーム STEP:0">
  <span>表示されない場合はこちら→ <a href="https://codepen.io/tekunikaruza/pen/NWWWyea">
  Vue &amp; Vuetifyでのバリデーション付きフォーム STEP:0</a></span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## 解説

以下のコードをベースにします。

```html:HTML
<div id="app">
  <v-app>
    <v-card>
      <v-card-title>STEP:0 フォーム</v-card-title>
      <v-card-text>
        <v-text-field
          v-model="text1"
          label="入力必須で文字数制限のあるテキストフィールド"
        >
        </v-text-field>
        <v-text-field
          v-model="text2"
          label="入力必須のみあるテキストフィールド"
        >
        </v-text-field>
        <v-text-field
          v-model="text3"
          label="なんの制約もないテキストフィールド"
        >
        </v-text-field>
      </v-card-text>
      <v-divider></v-divider>
      <v-card-actions>
        <v-btn text v-on:click="submit">送信する</v-btn>
        <span v-if="success">送信成功！</span>
      </v-card-actions>
    </v-card>
  </v-app>
</div>
```

```javascript:javascript
new Vue({
  el: '#app',
  vuetify: new Vuetify(),
  data: {
    // 各テキストボックスの値
    text1: "",
    text2: "",
    text3: "",
    
    // 送信が成功したかどうかのフラグ
    success: false,
  },
  methods: {
    // 送信を試みるメソッド（「送信する」がクリックされたときに呼ばれる）
    submit() {
      this.success = true;
    }
  }
});
```

Vuetifyを使用しているので、だいたいのコントロールは`v-`から始まる名前のタグになります。

入力必須や文字数制限のあるテキストボックスや、なんの制約もないテキストボックスも存在します。本来は、「送信する」をクリックすると、すべてのテキストボックスの入力検証をクリアしたときのみ、「送信成功！」と表示されてほしいのですが、今は、どんな状態でも「送信成功！」と表示されてしまします。

![STEP0完成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84550/4d99b980-71d6-582b-5bdf-10d0b2af8349.gif)

# STEP1: 各テキストボックスに制約をつける

このSTEPでは、各テキストボックスに入力規則を設けます。これにより、各テキストボックスの値が規則にそぐわない場合は、エラー表示させることができます。

## デモ

<p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="html,result" data-user="tekunikaruza" data-slug-hash="PoooRmO" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Vue &amp;amp; Vuetifyでのバリデーション付きフォーム STEP:1">
  <span>表示されない場合はこちら→ <a href="https://codepen.io/tekunikaruza/pen/PoooRmO">
  Vue &amp; Vuetifyでのバリデーション付きフォーム STEP:1</a></span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## 解説

今回使用している`v-text-field`を含め、Vuetifyのすべてのフォームコントロールには、`rules`という属性を付与でき、反映させたいバリデーション用関数の配列を渡すことで、1つ以上の制約をつけることができます。

`rules`属性を付与する前に、あらかじめvue側で、バリデーション用関数を準備しておきます。
ここでは、`data`の中に、**必須入力**と**文字数制限**の規則を反映させるための関数を用意しました。

```javascript:javascript
...(略)...
  data: {
    // 各テキストボックスの値
    text1: "",
    text2: "",
    text3: "",

    // 送信が成功したかどうかのフラグ
    success: false,
    
    // ↓***** 新規追加

    // 入力規則
    required: value => !!value || "必ず入力してください", // 入力必須の制約
    limit_length: value => value.length <= 10 || "10文字以内で入力してください" // 文字数の制約

    // ↑***** 新規追加
  },
...(略)...
```

それぞれの関数はどちらとも、<font color="Red">**引数として入力された文字列が渡され、制約に合致していれば`true`を返し、合致していなければエラーメッセージの文字列を返す関数**</font>です。

> ここではアロー関数という書き方で書いています。アロー関数を見慣れない方は、下記記事が参考になります。
> [【JavaScript】アロー関数式を学ぶついでにthisも復習する話](https://qiita.com/mejileben/items/69e5facdb60781927929)
> また、`||`を使った書き方が見慣れない方は、下記ページが参考になります。
> [演算子 || の特殊な使用方法 [Java Script - 基本 - Tips]](http://www.24w.jp/study_contents.php?bid=javascript&iid=javascript&sid=default&cid=008)
> 基本的に入出力の形があっていれば、どんな関数の書き方でもいけるはず。。。

ここでは、`data`の中に書いていますが、`methods`の中に書いても構いません。例えば、他のプロパティと比較をして検証したい場合などもあると思いますので、そのときは`methods`の中に書く必要があります。

以上で、用意したバリデーション用関数を、各テキストボックスへ反映させます。最初に説明した、`rules`属性を付与することで、必要な規則を1個でも2個でも何個でも反映させることができます。

```html:HTML
...(略)...
        <!-- :rules属性を追加 -->
        <v-text-field
          v-model="text1"
          label="入力必須で文字数制限のあるテキストフィールド"
          :rules="[required, limit_length]"
        >

        <!-- :rules属性を追加 -->
        </v-text-field>
        <v-text-field
          v-model="text2"
          label="入力必須のみあるテキストフィールド"
          :rules="[required]"
        >

        <!-- 特に変更なし -->
        </v-text-field>
        <v-text-field
          v-model="text3"
          label="なんの制約もないテキストフィールド"
        >
...(略)...
```

`rules`属性の中で、さきほど作った関数を指定しています。（ですので、属性名の手前には、`:`コロンまたは、`v-bind:`をつける必要があります。詳しくは[Vueドキュメント参照](https://v1-jp.vuejs.org/guide/syntax.html#%E5%BC%95%E6%95%B0)。）

ご覧の通り、`rules`属性が受け付けるのは、配列です。1番目のテキストボックスには、`required`と、`limit_length`の2つの規則を付与しています。2番目は、`required`のみ。3番目は、特に規則がないため、`rules`属性も付与していません。

実際に、キーボードで入力してみると、バリデーションが動いていることがわかります。

![バリデーションの動き](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84550/5c758b13-80d8-776e-96f8-8d31bb9bff92.gif)

### 注意点: 必ず`v-model`を使う

必ず、`v-model`を使って、フォームの入力値とプロパティをバインドさせないと、STEP2での動作に不具合が生じる場合があることを確認しました。

### Tips: 文字数カウンタをつける

このままでも良いのですが、1番目のテキストボックスは、「10文字まで」という制限があるにもかかわらず、ユーザは、入力を始めた時点では、最大入力可能文字数が何文字かがわかりません。さきほど実装したバリデーション用関数が発動してエラーメッセージが出て初めて、最大文字数の制約を知ることになります。これはちょっと不親切です。

これを解決する手段として、`v-text-field`には、`counter`という属性を付与することができます。これを使うと、その名の通りテキストボックスにカウンタが表示され、あと何文字入力できるか、ひと目でわかるようになります。

```html:HTML
...(略)...
        <!-- counter属性を追加 -->
        <v-text-field
          v-model="text1"
          label="入力必須で文字数制限のあるテキストフィールド"
          :rules="[required, limit_length]"
          counter="10"
        >
...(略)...
```
![カウンタの動き](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84550/d65101c9-aec2-e5a2-10e3-be46d3b00392.gif)

注意点としては、`rules`属性を使わずに、`counter`属性だけを使ったとしても、入力文字数の制約自体はなく、実質何文字でも入力できる点です。文字数制限をかけたいときは、必ず`rules`属性も併せて使ってください。

# STEP2: フォーム全体をバリデーションチェック

STEP1が終わった時点で、各テキストボックスには入力規則をつけることができました。しかし、この時点でも、不正値が入力されていたとしても、「送信する」をクリックすると、送信成功となってしまいます。

![送信成功してしまう](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84550/20801dfa-dc2d-5e16-42f5-113921d1bd31.gif)

このSTEPでは、これを解決します。

## デモ

<p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="html,result" data-user="tekunikaruza" data-slug-hash="gOOOeEz" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Vue &amp;amp; Vuetifyでのバリデーション付きフォーム STEP:2">
  <span>表示されない場合はこちら→ <a href="https://codepen.io/tekunikaruza/pen/gOOOeEz">
  Vue &amp; Vuetifyでのバリデーション付きフォーム STEP:2</a> </span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## 解説

考え方としては、**すべてのテキストボックスのバリデーションが通過しているかを確認し、通過している場合のみ、その先の処理を進める**、という流れになります。

まず、現時点では、各テキストボックスが個々でバリデーションチェックをしているだけになりますので、これらを`v-form`タグで囲って、**1つのフォームであることを認識させます**。この`v-form`に対しては、後ほどVue側からアクセスしますので、`ref`属性を使って名前をつけておきます。（ここでは`test_form`）

```html:HTML
...(略)...
      <v-card-text>
        <!-- ↓ 新規追加 -->
        <v-form ref="test_form">
        <!-- ↑ 新規追加 -->
          <v-text-field
            v-model="text1"
            label="入力必須で文字数制限のあるテキストフィールド"
            :rules="[required, limit_length]"
            counter="10"
          >
          </v-text-field>
          <v-text-field
            v-model="text2"
            label="入力必須のみあるテキストフィールド"
            :rules="[required]"
          >
          </v-text-field>
          <v-text-field
            v-model="text3"
            label="なんの制約もないテキストフィールド"
          >
          </v-text-field>
        <!-- ↓ 新規追加 -->
        </v-form>
        <!-- ↑ 新規追加 -->
      </v-card-text>
...(略)...
```

次に、Vue側では、「送信する」がクリックされたときに、**`v-form`内のすべてのコントロールのバリデーションが通過しているかを確認する処理**を追記します。

```javascript
...(略)...
  methods: {
    // 送信を試みるメソッド（「送信する」がクリックされたときに呼ばれる）
    submit() {
      // ↓***** 新規追加 & 修正
      if (this.$refs.test_form.validate()) {
        // すべてのバリデーションが通過したときのみ
        // if文の中に入る
        this.success = true;
      } else {
        this.success = false;
      }
      // ↑***** 新規追加 & 修正
    }
  }
...(略)...
```

`test_form`という名前をつけてあげた`v-form`に対して、`validate()`関数を呼ぶことで、`v-form`で囲まれた全コントロールのバリデーションチェックをしてくれます。そして、もし通過しなかったコントロールがあった場合、`false`が返され、同時に、該当コントロールをエラー表示にしてくれます。
すべて通過できた場合のみ、`true`が返されますので、そのときは、先の処理（ここでは「送信成功！」と表示させる）を進めさせます。

![STEP2完成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84550/8e676314-80f4-0dec-9866-41e5ccc695eb.gif)

なお、`v-form`には、`validate()`関数を含む3つの関数があります。

| 関数名 | 動作 |
|:------|:----|
| `validate()` | バリデーションチェックを実施します。すべて通過した場合は`true`を返します。そうで無い場合は`false`を返すと同時に、該当コントロールをエラー表示させます。 |
| `resetValidation()` | バリデーションチェックをリセットします。使いみちとしては、エラー表示を一旦すべて解除させたときなど。返り値はなし。 |
| `reset()` | すべての入力内容を初期値に戻します。また、バリデーションチェックもリセットされます。返り値はなし。|

以上で、目標としていたフォームを作ることができました！

# まとめ

基本的に、`rules`属性と`v-form`を使えばOKということがわかりました。