一休.com Developers Blog

一休のエンジニア、デザイナー、ディレクターが情報を発信していきます

VeeValidate 2から3へのアップデート

f:id:igatea:20191202001546p:plain

この記事は、一休.com Advent Calendar 2019の3日目の記事です。

qiita.com

宿泊事業本部のいがにんこと山口です。id:igatea
UIUXチームでフロントエンドをメインに開発しています。

一休の宿泊予約サイト の一部のフォームではVue.js、およびVeeValidateを用いてフォームのバリデーションを実装しています。
そのVeeValidateのバージョンを 2.2.15 から 3.0.11 へ移行しました。
VeeValidateはメジャーバージョンが2と3では大きく仕様が変わり、破壊的変更が多数入っています。
この記事ではVeeValidateのV2とV3の記述の比較を行い、VeeValidateのアップデートの参考になる情報をまとめたいと思います。

ライブラリバージョン情報

  • Vue.js 2.6.10
  • VueValidate 2.2.15 → 3.0.11

また先日、公式からもマイグレーションガイドが公開されました。
そちらも併せてご覧ください。 logaretm.github.io

破壊的変更

まずV2からV3に移行するにあたり破壊的変更によって影響を受けるものを挙げていきましょう。

  • Validatorクラスの廃止
  • v-validateの廃止
  • data-vv-asの廃止
  • ErrorBagクラスの廃止
  • $validatorプロパティの廃止

今までVeeValidateを使用していた方はかなり驚くのではないかと思います。
破壊的変更にあわせて関数ベースのAPIが公開されています。
パフォーマンス改善、コンポーネントの可読性、メンテナンス性の改善のためにこのような変更が行われました。
詳しくはバージョンアップのIssueで作成者が説明されています。
VeeValidate v3.0 🚀 · Issue #2191 · logaretm/vee-validate · GitHub

バリデーションルールの定義

基本形

V2

V2ではValidator.extendを使用してバリデーションルールの登録を行っていました。
またバリデーションエラー時のメッセージはgetMessageという関数を定義しています。

// 各バリデーション定義 ./Validations/hoge.js
export default {
  validate, // 値を検証する関数
  getMessage: field => `${field}の形式が正しくありません。`,
};

// 登録側 ./index.js
import VeeValidate from 'vee-validate';
import hoge from './Validations/hoge';

VeeValidate.Validator.extend('hoge', hoge);

V3

V3ではextend関数を使用してバリデーションルールの登録を行います。
またバリデーションエラー時のメッセージはmessageという名前で定義するようになりました。

// 各バリデーション定義 ./Validations/hoge.js
export default {
  validate, // 値を検証する関数
  message: field => `${field}の形式が正しくありません。`,
};

// 登録側 ./index.js
import { extend } from 'vee-validate';
import hoge from './Validations/hoge';

extend('hoge', hoge);

値を受け取るパターン

V2

export default {
  validate,
  getMessage: (field, maxByte) =>
    `${field}は全角${maxByte / 2}文字以内で入力してください`,
};

V3

V3では値を受け取るときにparamsの指定をする必要があります。

export default {
  params: ['max'],
  validate: (value, { max }) => validate(value, max),
  message: (field, { max }) =>
    `${field}は全角${max / 2}文字以内で入力してください`,
};

また、そのままvalidateメソッドからエラーメッセージを返すことが可能です。
{_field_} というフィールド名を表示するプレースホルダーも使用可能です。

export default function(value) {
  const invalidStr = checkStrXSS(value);
  if (invalidStr) {
    return `{_field_}に禁止文字${invalidStr}が含まれています。`;
  }
  return true;
}

標準バリデーターの再実装

V3ではライブラリの容量削減のため下記の標準バリデーターが削除されています。

  • before
  • credit_card
  • date_between
  • date_format
  • decimal
  • ip
  • ip_or_fqdn
  • url

例えばクレジットカードのバリデーターなどが削除されているので、適宜再実装の必要があります。
V2の内部では validator.js というものを使用しておりそのライブラリを使って同じものが再実装できます。
vee-validate/credit_card.js at 2.2.15 · logaretm/vee-validate · GitHub

import isCreditCard from 'validator/lib/isCreditCard';
/**
 * クレジットカードのバリデーション
 */
const validate = value => isCreditCard(String(value));

export default {
  validate,
  message: field => `${field}が正しくありません`,
};

v-validateを全てValidationProviderに

これが一番大規模な作業が必要になるところだと思います。
v-validateを使用しているところを全てValidationProviderに置き換えます。
ValidationProviderはバージョン2.1のときに実装されたバリデーションコンポーネントです。
バージョンが3になりv-validateが廃止された今、唯一のバリデーションを実行するための方法となっています。
またエラーの表示方法によってはValidationObserverも組み合わせる必要があるでしょう。

V2

<input v-validate="'required'" data-vv-as="姓" name="lastName" v-model="lastName" type="text">
<span>{{ errors.first('lastName') }}</span>

V3

ValidationProviderではv-validateはrulesに、data-vv-asはnameとなります。
エラーはScoped slot dataから取得するようになりました。
v-slot="{ errors }" というところですね。グローバルなerrorsを使用しなくなりました。
ValidationProvider内のエラーがすべてそのまま配列に入っています。
これでシンプルに errros[0] という記述だけでエラーを取れるようになり、nameを引数に指定して取る必要はなくなりました。

<validation-provider name="姓" rules="required" v-slot="{ errors }">
  <input name="lastName" v-model="lastName" type="text">
  <span>{{ errors[0] }}</span>
</validation-provider>

親子コンポーネントでのバリデーション結果共有

親コンポーネントと子コンポーネントでバリデーション結果を共有するということがあると思います。
そういう時V2では$validatorをinjectに設定して実装していました。
しかしV3では$validatorは存在しません。
その代わりにValidationObserverで親コンポーネントを囲むことで実現できます。

<!-- 親 -->
<template>
  <validation-observer ref="validationObserver" tag="div" v-slot="{ errors }">
    <!-- バリデーションがある子コンポーネント -->
    <child-component />
  </validation-observer>
</template>

<!-- 子 -->
<template>
  <validation-provider rules="required">
    <input type="text" v-model="hoge">
  </validation-provider>
</template>

子コンポーネントも含めたエラーがerrorsに入ります。
子コンポーネントで親コンポーネントのエラーを使用するという場合はpropsとして送る必要があります。

validateメソッド

$validator が廃止になったので全てのバリデーション結果を取得するメソッドも変わっています。

V2

const isValid = this.$validator.validateAll();

V3

V3ではValiationObserverを$refsで取得することで、そのコンポーネント内のValidationObserver、ValidationProviderのエラーの有無を以下のメソッドで検知することができます。

const isValid = this.$refs.validationObserver.validate();

VueSFCの例

<template>
  <validation-observer ref="validationObserver" tag="div">
    <validation-provider rules="required">
      <input type="text" v-model="hoge">
    </validation-provider>
    <validation-provider rules="required">
      <input type="text" v-model="fuga">
    </validation-provider>
  </validation-observer>
</template>
<script>
export default {
  methods: {
    submit() {
      const isValid = this.$refs.validationObserver.validate();
      // 後続処理
    } 
  },
}
</script>

ErrorBag廃止の対応

V2ではエラーが ErrorBag というもので返ってきていました。
ですがV3の v-slot="{ errors }" で取得したエラーは { [エラーのフィールド名]: [エラーメッセージの配列] } という形のオブジェクトとして格納されます。
注意として、ここのエラーフィールド名というのはinputのnameではなくValidationProviderに指定したnameとなります。
なのでErrorBagからinputのnameが取得することができなくなりエラーからはinputを特定するといったことが困難になりました。
特定のエラー情報を元にそのinputにフォーカスするということがそのままだとできないわけです。
ここではその対応の一例としてclassを振って判定可能にする方法を紹介します。
エラーになったフィールドには特定のクラスを割り当ててそれを活用してフォーカスするようにします。

errorKeyがValidationProviderのnameに指定した値(バリデーションエラー時に表示されるフィールド名)、errorIndexがそのコンポーネント内での何番目のエラーかを表します。

// componentのmethods内に定義
focusError({ errorKey, errorIndex }) {
  const validationProvider = this.$refs.validationObserver.refs[errorKey];

  if (validationProvider) {
    validationProvider.$el
      .getElementsByClassName('errorField')
      [errorIndex].focus();
    return;
  }

  // ValidationObserverで囲われたValidationProviderがエラーの場合、observersにValidationObserverが格納されその内部のエラーとなっている入力欄を探す
  // エラーとなっている入力欄をどちらにフォーカスするかは配列の順番に従う
  const validationObserver = this.$refs.validationObserver.observers.find(
    observer => observer.id === errorKey,
  );
  if (validationObserver) {
    validationObserver.$el
      .getElementsByClassName('errorField')
      [errorIndex].focus();
  }
},

observers などは公式ガイドに載っていないAPIなのでそれを踏まえたうえで使用してください。

エラーが出力されているかのテスト

V2

V2ではvalidateメソッドを叩いてバリデーションをかけ、$validatorからエラーを取り出してテストをしていました。

it('フリガナの必須項目チェックが機能しているか', async () => {
  const wrapper = shallowMount(HogeComponent, {
    sync: false,
  });
  await wrapper.vm.$validator.validate();
  assert.strictEqual(wrapper.vm.$validator.errors.has('kanaSei'), true);
  assert.strictEqual(wrapper.vm.$validator.errors.has('kanaMei'), true);
});

V3

V3ではinputに値を入力して、DOM更新を行って、クラスから要素を取得することによってエラーがあるかどうかを検知するようになりました。
また、v-slotを使用してValidationObserver、ValidationProvider内にエラーが出力されるので、shallowMountのときはVeeValidateのコンポーネントがスタブにならないようにstubsにValidationObserver、ValidationProviderを指定する必要があります。

import flushPromises from 'flush-promises';
import { shallowMount } from '@vue/test-utils';
import { ValidationObserver, ValidationProvider } from 'vee-validate';

it('フリガナの必須項目チェックが機能しているか', async () => {
  const wrapper = shallowMount(HogeComponent, {
    sync: false,
    stubs: { ValidationObserver, ValidationProvider },
  });
  wrapper.find('input[name="kanaSei"]').setValue('');
  wrapper.find('input[name="kanaMei"]').setValue('');
  await flushPromises();
  const errors = wrapper.findAll('.errorText');
  assert.strictEqual(errors.length, 2);
});

個別のバリデーションメッセージをテストする

バリデーションがこのように定義されていたとしたら、フィールド名が適用された状態のエラーメッセージをテストすることができません。

export default function(value) {
  const invalidStr = checkStrXSS(value);
  if (invalidStr) {
    return `{_field_}に禁止文字${invalidStr}が含まれています。`;
  }
  return true;
}

VeeValidateのルールにのっとってフィールド名も正しく入っているかをテストしたい場合は以下のように書くことで実現できます。

import { extend, validate } from 'vee-validate';

extend('noxss', noxss);
it('validate() - JavaScriptっぽい文字列が含まれないようバリデーション', async () => {
  // OK
  const ok = await validate('ふつうのコメント', 'noxss');
  assert(!ok.errors[0]);

  // NG
  const ng = await validate(
    '<Script>//JSっぽい文字列が含まれるコメント</Script>',
    'noxss',
  );
  assert(ng.errors[0]);
});

最後に

この記事がVeeValidateのアップデート時に皆様の役に立てば幸いです。
また今回書かせていただいたVeeValidateの話を12月23日のRoppongi.vueでお話させていただきます。
弊社一休が会場提供させていただくのでこの機会にぜひお越しください!

roppongi-vue.connpass.com