【vee-validate】v3 → v4マイグレーション【Options API】

:::note info
viviONグループでは、DLsiteやcomipoなど、二次元コンテンツを世の中に届けるためのサービスを運営しています。
ともに働く仲間を募集していますので、興味のある方は[こちら](#punch-一緒に二次元業界を盛り上げていきませんか)まで。
:::

## 🦺 概要

現在担当しているPJでVue2 → Vue3へのアップグレードを進めていて（今更）、PJで使用していた[vee-validate](https://vee-validate.logaretm.com/v3/)もv3 → v4へアップグレードする必要がありました。

今回はひとまずOptions APIのままで進めることになったのですが、vee-validate v4へのマイグレーションに関してOptions APIでの情報があまり存在せずかなり苦労したので、同じ轍を踏んでしまわないようまとめてみました。

## 🦺 環境

Vue：v2.7.14 → v3.1.0
vee-validate：v3.4.15 → v4.13.2

## 🦺 アップグレード

まずはvee-validateをv4へアップグレードします。
また、v3で使用していた`vee-validate/dist/rules`が廃止され別のパッケージにまとめられているので、それも併せてインストールします。

```
npm i vee-validate @vee-validate/rules
```

:::note warn
必要な方は`@vee-validate/i18n`も併せてインストールしてください
:::

あとはimport元を一括置換すればOK
例）

```js
- import { ValidationProvider, ValidationObserver, extend } from 'vee-validate';
- import { required, size, ext, email, max } from 'vee-validate/dist/rules';
+ import { Field as ValidationField, Form as ValidationForm, defineRule } from 'vee-validate';
+ import { required, size, ext, email, max } from '@vee-validate/rules';
```

FieldやdefineRuleについては後述します。

## 🦺 マイグレーション

ここから、具体的な記法のマイグレーションについて記載します。

### Field, Form

先ほどでてきた`ValidationField`, `ValidationForm`に関してですが、それぞれv3の`ValidationProvider`と`ValidationObserver`に対応していると考えてもらって大丈夫です。

importを修正したら、template内のProvider, ObserverをすべてField, Formに置換します。

また、v4からはFieldのバリデーション発火タイミングを`validateOnHoge`で制御できます（基本いじらなくても大丈夫なはず）

https://vee-validate.logaretm.com/v4/api/field#props

:::note
Fields（Provider）へのrulesの指定方法についてはv3とほとんど変わらないので、基本そのままで問題ないです
:::

### defineRule

これはv3でいう`extend`に対応しているものですが、ちょっと書き方が変わります。

例）

```js
- extend('max', {
-   ...max,
-   message: '{_field_}は、{length}文字以下で指定してください。'
- });
+ defineRule('max', (value, params, { field }) => {
+   return max(value, params) || `${field}は、${params}文字以下で指定してください。`
+ });
```

`defineRule`のcallback関数の引数は、それぞれ

1. 実際の値
2. 設定している閾値の配列※
    - なにも設定していない場合は`null`
3. validateのcontext
    - `FieldValidationMetaInfo`型に紐づいています
        - field
        - form
        - label
        - name
        - rule
        - value

このようになっています。

※`params`の中身はルールによって変わることがあるので、適宜必要なものを取り出します。

https://vee-validate.logaretm.com/v4/guide/global-validators#available-rules

### v-slot

v3のProviderは入力要素の値を自動で監視してくれていましたが、v4のFieldは明示的に入力要素の値を紐づける必要があります。

Fieldの`modelValue`に対して、監視したい値をバインドすればOKです。

```vue
- <ValidationProvider name="ユーザーネーム" rules="required|max:50" v-slot="{ errors }">
+ <ValidationField :model-value="name" name="ユーザーネーム" rules="required|max:50" v-slot="{ errors }">
    <input
      type="text"
      id="userName"
      v-model="name"
      placeholder="ユーザーネームを追加"
      class="input"
      :class="{'input-error' : errors.length}"
    />
    <p v-if="errors.length">
      {{ errors[0] }}
    </p>
- </ValidationProvider>
+ </ValidationField>
```

公式的にはFieldに存在する[field](https://vee-validate.logaretm.com/v4/api/field#Rendering-Complex-Fields-with-Scoped-Slots)スロットを使ってほしいらしいです（「これをbindするだけで必要な情報が全部渡せるよ」的なことが書いてあります）。

が、field経由でv-modelの`modalValue`が上手く渡せていない場合があったため、従来通り入力要素側にv-modelをバインドし、Fieldに`modalValue`を直接渡す形にすることでうまく動きました。

ちなみに、この形なら`VueTagsInput`などの外部コンポーネントや独自のカスタムコンポーネントでも同様に対応できます。

↓VueTagsInputの例（余計な部分は省略してます）

```vue
<VueTagsInput
  v-model="typingTag"
  @tags-changed="(newTags) => changeTags(newTags)"
>
  <template #between-elements>
  </template>
  <div
    slot="autocomplete-item"
    slot-scope="props"
  >
  </div>
</VueTagsInput>
<ValidationField
  :model-value="selectedTags.length"
  name="タグ"
  rules="max_length:10"
  v-slot="{ field, errors }"
  as="div"
>
  <input :="field" type="hidden" />
  <p v-if="errors.length">{{ errors[0] }}</p>
</ValidationField>
<ValidationField
  :model-value="addingTag"
  name="タグのテキスト"
  rules="tagMax:30"
  v-slot="{ field, errors }"
  as="div"
>
  <input :="field" type="hidden" />
  <p v-if="errors.length">{{ errors[0] }}</p>
</ValidationField>
```

VueTagsInputにバインドしている入力中のタグテキストや、選択中のタグの配列などをFieldに紐づけて、その値を使ってバリデーションしています。

:::note warn
本来vue-tags-inputもVue3の破壊的変更の影響により移行必須ですが、今回はVue3に対応してForkされたものを暫定採用しています
https://github.com/sipec/vue-tags-input
:::

### state

v3でvalidation stateを使ってバリデーションタイミングの制御をしていた場合、v4では[meta slot](https://vee-validate.logaretm.com/v4/api/field#slots)に変更になっているのでそちらに置き換えます。

ただ、v4からは監視できる状態が

- touched
- dirty
- valid
- validated
- pending

のみになっています。

### as

v3の`tag=”div”` はすべてasに置き換わっているので`as=”div”`に置換します。以上。

### validate()

v3ではObserverのrefから得られる`validateWithInfo` を使ってform送信前の全体バリデーションを行っていたのですが、この関数も廃止されているので、代わりに`validate`に置き換えます。

```jsx
- this.$refs.observer.validateWithInfo().then(({ isValid, fields }) => {
-   this.isValid = isValid;
-   this.errorsMessage = [];
-   Object.values(fields).filter(({ failedRules }) => {
-     if (Object.keys(failedRules).length) {
-       this.errorsMessage.push(Object.values(failedRules));
-     }
-   });
- });
+ this.$refs.observer.validate().then(({ valid, errors }) => {
+   this.isValid = valid;
+   this.errorsMessage = Object.values(errors) ?? [];
+ });
```

従来はfieldsの中身からmessageを引っ張り出していましたが、validateから得られるerrorsは`name: message`のシンプルなオブジェクトになっているので、値をそのまま取り出して突っ込む形に修正します。

また、エラーフラグ名がisValidではなくvalidになっているのでこちらも併せて修正します。

↓ちなみに、v3のvalidateWithInfoから返ってきてたfieldsの中身はこんな感じです。

```json
["$name": {
    "id": "hoge",
    "name": "$name",
    "failedRules": {
      "required": "ここにメッセージが入ってます"
      "any rule1": "同上",
      "any rule2": "同上"
    },
    "untouched": false,
    "touched": true,
    "dirty": true,
    "pristine": false,
    "valid": false,
    "invalid": true,
    "validated": true,
    "pending": false,
    "required": true,
    "changed": true,
    "passed": false,
    "failed": true
}]
```

## 🦺 最後に

Vue2がEOLになって一年が過ぎようとしていますが、なんとか記事が間に合ったのでよかったです :relaxed:（間に合ってない）

それでは、良いお年を :wave: 

## :punch: 一緒に二次元業界を盛り上げていきませんか
株式会社viviONでは、フロントエンドエンジニアを募集しています。

https://vivion.jp/recruit/

また、フロントエンドエンジニアに限らず、バックエンド・SRE・スマホアプリなど様々なエンジニア職を募集していますので、ぜひ採用情報をご覧ください。