VeeValidate を custom component + vuex + custom rule で使う

# はじめに
Vue.js で作っているプロジェクトでフォームを作る場合には、バリデーションの実装を VeeValidate を用いることが多いです。

- VeeValidate: http://vee-validate.logaretm.com/

非常に便利なプラグインなのですが、デフォルト設定のまま利用するとフォーム1つに対してのバリデーションチェックのみしか出来なかったり、Atomic Design でフォームの input, select 要素を custom component にした場合に動作しなかったりと、
実運用でハマりポイントがいくつかあるので段階をおって説明したいと思います。

**作るもの**

- 注文入力欄
  - 入力必須
  - 最大文字数: 10文字


## Level 1
デフォルトルールで特にコンポーネントを分ける必要が無い場合。

### 画面側

```vue:Order.vue
<template>
  <input
    v-validate="{ required:true, max:10 }"
    :class="{ 'error': errors.has('order') }"
    name="order"
    :placeholder="上ハラミ"
    type="text"
  >
</template>
```
*（焼肉食いたいですね）*

これはドキュメントを見ればすぐ書けて簡単ですね。


## Level 2
別の画面でinput要素を使うようになり custom component にした場合。

###  フォーム側
```vue:InputText.vue
<template>
  <input
    v-model="inputValue"
    :class="className"
    :name="name"
    :placeholder="placeholder"
    type="text"
    @input="$emit('input', $event.target.value)"
  >
</template>

<script>
export default {
  name: 'InputText',
  props: {
    className: {
      type: String, 
      default: ''
    },
    placeholder: { 
      type: String,
      default: 'placeholder'
    },
    name: { 
      type: String, 
      default: ''
    },
    value: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      inputValue: null
    };
  },
  watch: {
    value: function(newValue) {
      this.inputValue = newValue;
    }
  },
  mounted() {
    this.inputValue = this.value;
  }
};
</script>
```
親の `v-model` は `props value` なので初回時の`mounted`、変更時の `watch` を使って、`data inputValue` にセットすることで自コンポーネントの `v-model` としています。
また `@input` を使うことで親の `v-model` に値を反映しています（checkbox だと `@change`）。


### 画面側

```vue:Order.vue
<InputText
  v-validate="{ required:true, max:10 }"
  v-model="order"
  :class-name="{ 'error': errors.has('order') }"
  name="order"
  data-vv-value-path="inputValue"
  placeholder="上ハラミ"
/>
```

`data-vv-value-path` に子コンポーネントの `v-model` の変数名を入れないと（この場合だと`inputValue`）、バリデーションに渡す値が遅延するので設定必須です（記載ないと親側の複数箇所でnextTickをする必要があります）

- フォームの扱い Vuex: https://vuex.vuejs.org/ja/forms.html
- API | VeeValidate: https://vee-validate.logaretm.com/api.html

## Level 3
画面に複数フォームになり、vuex を導入する必要がある段階。
それに加えてカスタムルールを用いてバリデーションをする場合。

### 画面側

```vue:Order.vue
<template>
  <div 
    v-for="(food, index) in foods"
    :key="index"
  >
    <InputText
      v-validate="{ required:true, max:10, custom-rule: [index] }"
      v-model="'order' + index"
      :class-name="{ 'error': errors.has('order' + index) }"
      :name="'order' + index"
      data-vv-value-path="inputValue"
      data-vv-as="注文"
      placeholder="上ハラミ"
      @input="onInput"
    />
  </div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import store from '~/src/store';

export default {
  name: 'Orders',
  store,
  data() {
    return {};
  },
  computed() {
    ...mapState(['orders']),
  },
  watch: {
    // 変更時に全てのバリデーションチェック
    orders() {
      this.checkCustomValidate();
    }
  },
  created() {
    // カスタムルールの追加
    this.$validator.extend('custom-rule', {
      getMessage: () => '拡張バリデーションのメッセージ',
      validate: () => this.orders.length > 10
    });
  },
  mounted() {},
  methods: {
    ...mapActions(['setOrders']),

    /**
     * フォームの値をstoreに反映
     */
    onInput(e) {
      this.setOrders(e.target.value)
    },

    /**
     * カスタムルールの確認
     */
    checkCustomValidate() {
      const errorBag = this.$validator.errors;
      const errors = errorBag.items.filter(item => item.rule === 'custom-rule');
      if (size(errors) > 0) {
        this.$validator.validateAll();
      }
    }
  }
};
</script>

```

vuex を使う場合には画面側で `@input` を使って `store` に反映をする必要があります。
またフォーム入力時に関連する他のフォームについてもバリデーションエラーを表示したい場合には、`watch` を使い `store` または `data` の変更を監視することで、 `validateAll` を使うことで、より実用に近いバリデーションの実装が可能になります。

VeeValidateでルールを定義せずに `errors` に対して `add`, `update` する方法も試してみましたが、そちらはデフォルトルールと一緒に定義した場合に挙動がおかしくなってしまったので使わないようにしました。

GWになったら書きたいなと思っていたので早速エントリーしてみました:muscle:
天気も良く最高ですね！みなさんよい休日をお過ごしください:sunglasses: