はじめに
Vue.js で作っているプロジェクトでフォームを作る場合には、バリデーションの実装を VeeValidate を用いることが多いです。
- VeeValidate: http://vee-validate.logaretm.com/
非常に便利なプラグインなのですが、デフォルト設定のまま利用するとフォーム1つに対してのバリデーションチェックのみしか出来なかったり、Atomic Design でフォームの input, select 要素を custom component にした場合に動作しなかったりと、
実運用でハマりポイントがいくつかあるので段階をおって説明したいと思います。
作るもの
- 注文入力欄
- 入力必須
- 最大文字数: 10文字
Level 1
デフォルトルールで特にコンポーネントを分ける必要が無い場合。
画面側
<template>
<input
v-validate="{ required:true, max:10 }"
:class="{ 'error': errors.has('order') }"
name="order"
:placeholder="上ハラミ"
type="text"
>
</template>
(焼肉食いたいですね)
これはドキュメントを見ればすぐ書けて簡単ですね。
Level 2
別の画面でinput要素を使うようになり custom component にした場合。
フォーム側
<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
)。
画面側
<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 を導入する必要がある段階。
それに加えてカスタムルールを用いてバリデーションをする場合。
画面側
<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になったら書きたいなと思っていたので早速エントリーしてみました
天気も良く最高ですね!みなさんよい休日をお過ごしください