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

<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

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

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

![Sample](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84550/68eece58-bbcd-b514-aad2-ea49f5383ad4.gif)

なお、本記事では、Options APIではなく、Composition APIを使用しています。

# バージョン

* Vue 3.2.33
* Vuetify 3.0.0 Beta 3

:::note warn
Vuetifyは、Vue3.xに対応したVuetify3.xがまだ正式リリースされていません。(2022/6/3現在)
ですので、この記事ではベータ版を使用しています。今後、仕様が変更となる可能性がありますのでご注意ください。
:::

# STEP0: ベース

## デモ

<p class="codepen" data-height="550" data-default-tab="result" data-slug-hash="qBVoLMb" data-user="tekunikaruza" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/tekunikaruza/pen/qBVoLMb">
  Vue3 &amp; Vuetify3でのバリデーション付きフォーム STEP:0</a> by テクニカル座 (<a href="https://codepen.io/tekunikaruza">@tekunikaruza</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>

## 解説

本記事では、以下のコードをベースに、バリデーション付きのフォームを作っていきます。

```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="state.text1"
          label="入力必須で文字数制限のあるテキストフィールド"
        >
        </v-text-field>
        <v-text-field
          v-model="state.text2"
          label="入力必須のみあるテキストフィールド"
        >
        </v-text-field>
        <v-text-field
          v-model="state.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="state.success">送信成功！</span>
      </v-card-actions>
    </v-card>
  </v-app>
</div>
```

```javascript:JavaScript
const app = Vue.createApp({
  setup() {
    // リアクティブデータ
    const state = Vue.reactive({
      // 各テキストボックスの値
      text1: '',
      text2: '',
      text3: '',
      // 送信が成功したかどうかのフラグ
      success: false,
    })
    
    // 送信を試みるメソッド（「送信する」がクリックされたときに呼ばれる）
    const submit = () => {
      state.success = true
    }
    
    return {
      state,
      submit,
    }
  },
})

app.use(Vuetify.createVuetify())
app.mount('#app')
```

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

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

![STEP 0](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84550/fda6a442-2744-89a7-496f-b956aa45bcf0.gif)

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

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

## デモ

<p class="codepen" data-height="550" data-default-tab="result" data-slug-hash="XWzEGba" data-user="tekunikaruza" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/tekunikaruza/pen/XWzEGba">
  Vue3 &amp; Vuetify3でのバリデーション付きフォーム STEP:1</a> by テクニカル座 (<a href="https://codepen.io/tekunikaruza">@tekunikaruza</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>

## 解説

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

`rules`属性を付与する前に、あらかじめvue側で、バリデーション用関数を準備しておきます。ここでは、`setup()`内に、必須入力と文字数制限の規則を反映させるための関数（`requiredValidation()`と`limitLengthValidation()`）を用意しました。return時に、当該関数も含めています。

```javascript:JavaScript
const app = Vue.createApp({
  setup() {
    // リアクティブデータ
    const state = Vue.reactive({
      // 各テキストボックスの値
      text1: '',
      text2: '',
      text3: '',
      // 送信が成功したかどうかのフラグ
      success: false,
    })
    
    // ↓***** 新規追加
    
    // バリデーション関数
    const requiredValidation = (value) => !!value || '必ず入力してください' // 入力必須の制約
    const limitLengthValidation = (value) => value.length <= 10 || '10文字以内で入力してください' // 文字数の制約
    
    // ↑***** 新規追加
    
    // 送信を試みるメソッド（「送信する」がクリックされたときに呼ばれる）
    const submit = () => {
      state.success = true
    }
    
    return {
      state,
      // ↓***** 新規追加
      requiredValidation,
      limitLengthValidation,
      // ↑***** 新規追加
      submit,
    }
  },
})

// ***** (後略) *****
```

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

:::note warn
`||`を使った書き方が見慣れない方は、下記ページが参考になります。
[演算子 || の特殊な使用方法 [Java Script - 基本 - Tips]](http://www.24w.jp/study_contents.php?bid=javascript&iid=javascript&sid=default&cid=008)
基本的に入出力の形があっていれば、どんな関数の書き方でもいけるはず。。。
:::

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

```html:HTML
<!-- (前略) -->

        <!-- :rules属性を追加 -->
        <v-text-field
          v-model="state.text1"
          label="入力必須で文字数制限のあるテキストフィールド"
          :rules="[requiredValidation, limitLengthValidation]"
        >
        </v-text-field>

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

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

<!-- (後略) -->
```

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

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

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

![STEP 1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84550/a74ed30d-c08e-f0a5-2abf-77af7ca18e61.gif)


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

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

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

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

![STEP1 TIPS](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84550/7d0f3414-8f66-c92e-f7ed-47484a417184.gif)

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

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

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

![STEP2 前提](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84550/9bd2645f-87ee-d536-b935-537921df02ed.gif)

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

## デモ

<p class="codepen" data-height="550" data-default-tab="result" data-slug-hash="WNMvPOO" data-user="tekunikaruza" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/tekunikaruza/pen/WNMvPOO">
  Vue3 &amp; Vuetify3でのバリデーション付きフォーム STEP:2</a> by テクニカル座 (<a href="https://codepen.io/tekunikaruza">@tekunikaruza</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>

## 解説

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

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

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

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

```javascript:JavaScript
const app = Vue.createApp({
  setup() {
    // リアクティブデータ
    const state = Vue.reactive({
      // ***** (中略) *****
      // 送信が成功したかどうかのフラグ
      success: false,
    })
    
    // ***** (中略) *****

    // ↓***** 新規追加
    // フォームコンポーネントの取得
    const testForm = Vue.ref()
    // ↑***** 新規追加
    
    // ↓***** 修正
    // 送信を試みるメソッド（「送信する」がクリックされたときに呼ばれる）
    const submit = async () => {
      // 検証
      const validResult = await testForm.value.validate()
      if (validResult.valid) {
        state.success = true
      } else {
        state.success = false
      }
    }
    // ↑***** 修正
    
    return {
      state,
      requiredValidation,
      limitLengthValidation,
      // ↓***** 新規追加
      testForm,
      // ↑***** 新規追加
      submit,
    }
  },
})

// ***** (後略) *****
```

まず、`testForm`を`setup()`内で扱うために、`const testForm = Vue.ref()`でコンポーネントを取得しています。同時に、`setup()`のリターン内に`testForm`を加えています。（Vue3の仕様上、リターンしないと、testFormの中身がundefinedになってしまいます。）

続いて、「送信する」がクリックされたときに呼ばれる`submit()`メソッドを修正します。
`testForm.value.validate()`と書いて、testFormの中身の`validate()`関数を呼び出します。この関数を呼ぶことで、`v-form`で囲まれた全コントロールのバリデーションチェックをしてくれます。この関数は非同期関数ですので、返り値を取得するために`await`をつけています。（なので、`submit()`には`async`をつけています。）
ここでは、返り値を`validResult`に格納しています。

`validResult`は、検証結果が格納されているオブジェクトになっており、`validResult.valid`には、検証を通過したかどうかの真偽値が入っています。すべてのコントロールが検証を通過した場合は`true`ですが、通過しなかったコントロールがあった場合は`false`となり、同時に、該当コントロールをエラー表示にしてくれます。
ここでは、`true`となった場合のみ、先の処理（ここでは「送信成功！」と表示させる）を進めさせています。

なお、`validResult.errorMessages`には、該当するエラーメッセージの配列が格納されていますので、すべてのエラーメッセージを決まった場所に表示させたい場合などに利用できます。

![STEP 2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84550/3b1406c4-0106-95fb-37d3-85cd7d333284.gif)

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

| 関数名 | 動作 |
|:--|:--|
| `validate()` | バリデーションチェックを実施します。返り値の中身は上述の通り。 |
| `resetValidation()` | バリデーションチェックをリセットします。使いみちとしては、エラー表示を一旦すべて解除させたときなど。返り値はなし。 |
| `reset()` | すべての入力内容を初期値に戻します。また、バリデーションチェックもリセットされます。返り値はなし。 |

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

# まとめ

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