1. ryo511

    No comment

    ryo511
Changes in body
Source | HTML | Preview
@@ -1,449 +1,445 @@
## 概要
VeeValidateで、入れ子のフォームを作る方法を解説します。
せっかちな方は以下のVeeValidate公式ドキュメントを読むのが早いです。
http://vee-validate.logaretm.com/examples.html#event-bus-example
## サンプルコード
あなたが通販サイトを作っていて、そのECサイトの購入フォームでは、以下の仕様に基づいて入力を要求するとします。
- 必須項目(会員情報)
- 氏名
- メールアドレス
- 電話番号
- 住所
- 選択項目
- 配送先住所(会員の住所と、配送先が異なる場合のみ使用)
-このようなフォームを、Vue.jsの単一ファイルコンポーネントを使って作成すると、以下のようになります。
-
-※プロジェクトの基本的な構成は[vue-cli](https://github.com/vuejs/vue-cli)を使って、`vue init webpack`しています。また、本記事で紹介したコードは[GitHub](https://github.com/ryo-utsunomiya/vee-validate-nested-form)でも公開しています。
-
<img width="252" alt="Screen Shot 2017-11-12 at 0.18.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/33450/4efdd6e2-f568-1399-a13f-9b63b8039c0d.png">
「Ship to different address」にチェックを入れると、別の住所の入力欄が表示されるようになっています。
未入力の状態で送信ボタンを押すと、バリデーションエラーが表示されます。
<img width="369" alt="Screen Shot 2017-11-12 at 0.39.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/33450/f91c1d60-d6d2-dfaf-5d24-addef1e3a984.png">
+このサンプルコードの基本的な構成は、[vue-cli](https://github.com/vuejs/vue-cli)で`vue init webpack`したものです。また、本記事で紹介したコードは[GitHub](https://github.com/ryo-utsunomiya/vee-validate-nested-form)でも公開しています。
-ここでは、バリデーションのために[vee-validate](https://github.com/baianat/vee-validate)を導入しています。
+また、バリデーションのために[vee-validate](https://github.com/baianat/vee-validate)を導入しています。
EntryFormコンポーネントのコードは以下のようになっています。
```html
<template>
<form action="">
<div>
<label for="name">Name</label>
<input
type="text"
name="name"
id="name"
v-model="name"
v-validate="'required'"
>
</div>
<div>
<label for="email">Email</label>
<input
type="email"
name="email"
id="email"
v-model="email"
v-validate="'required|email'"
>
</div>
<div>
<label for="tel">Tel</label>
<input
type="tel"
name="tel"
id="tel"
v-model="tel"
v-validate="'required'"
>
</div>
<div>
<label for="address">Address</label>
<input
type="text"
name="address"
id="address"
v-model="address"
v-validate="'required'"
>
</div>
<div>
<label for="shipToDifferentAddress">Ship to different address</label>
<input
type="checkbox"
name="shipToDifferentAddress"
v-model="shipToDifferentAddress"
id="shipToDifferentAddress"
>
</div>
<div v-show="shipToDifferentAddress">
<label for="shippingAddress">Shipping Address</label>
<input type="text"
name="shippingAddress"
id="shippingAddress"
v-model="shippingAddress"
v-validate="'required'"
>
</div>
<div v-show="errors.any()">
<p v-for="error in errors.collect()">{{ error }}</p>
</div>
<input type="submit" value="submit" @click.prevent="doValidation">
</form>
</template>
<script>
/* eslint-disable no-console */
export default {
name: 'entry-form',
data() {
return {
name: '',
email: '',
tel: '',
address: '',
shipToDifferentAddress: false,
shippingAddress: '',
};
},
methods: {
doValidation() {
this.$validator.validateAll();
if (this.errors.any()) {
console.log('cancel submission');
} else {
console.log('do submission');
}
},
},
};
</script>
```
-ここでは、簡略化のためにスタイルの制御などを省いていますが、実際のアプリケーションではさらに複雑な表現が必要とされることもあるでしょう。その場合、コードは肥大化していきます。
+ここでは、簡略化のためにスタイルの制御などを省いていますが、実際のアプリケーションではさらに複雑な表現が必要とされることもあるでしょう。その場合、コードは肥大化していき、保守性は低下していきます。
### リファクタリングと、それによって生じる問題
-入力フォームを会員情報と配送オプションに分割してみま
+保守性向上のため、入力フォームを会員情報と配送オプションに分割してみましょう
```html
<template>
<form action="">
<member-form :member="member"></member-form>
<option-form :option="option"></option-form>
<div v-show="errors.any()">
<p v-for="error in errors.collect()">{{ error }}</p>
</div>
<input type="submit" value="submit" @click.prevent="submit">
</form>
</template>
<script>
/* eslint-disable no-console */
import MemberForm from './MemberForm';
import OptionForm from './OptionForm';
export default {
name: 'entry-form',
components: {
MemberForm,
OptionForm,
},
data() {
return {
member: {
name: '',
email: '',
tel: '',
address: '',
},
option: {
shipToDifferentAddress: false,
shippingAddress: '',
},
};
},
methods: {
submit() {
this.$validator.validateAll();
if (this.errors.any()) {
console.log('cancel submission');
} else {
console.log('do submission');
}
},
},
};
</script>
```
```html
<template>
<div>
<div>
<label for="name">Name</label>
<input
type="text"
name="name"
id="name"
v-model="member.name"
v-validate="'required'"
>
</div>
<div>
<label for="email">Email</label>
<input
type="email"
name="email"
id="email"
v-model="member.email"
v-validate="'required|email'"
>
</div>
<div>
<label for="tel">Tel</label>
<input
type="tel"
name="tel"
id="tel"
v-model="member.tel"
v-validate="'required'"
>
</div>
<div>
<label for="address">Address</label>
<input
type="text"
name="address"
id="address"
v-model="member.address"
v-validate="'required'"
>
</div>
</div>
</template>
<script>
export default {
name: 'member-form',
props: [
'member',
],
};
</script>
```
```html
<template>
<div>
<div>
<label for="shipToDifferentAddress">Ship to different address</label>
<input
type="checkbox"
name="shipToDifferentAddress"
v-model="option.shipToDifferentAddress"
id="shipToDifferentAddress"
>
</div>
<div v-show="option.shipToDifferentAddress">
<label for="shippingAddress">Shipping Address</label>
<input
type="text"
name="shippingAddress"
id="shippingAddress"
v-model="option.shippingAddress"
v-validate="'required'"
>
</div>
</div>
</template>
<script>
export default {
name: 'option-form',
props: [
'option',
],
};
</script>
```
-コード量は増えましたが、コンポーネントが細かく分かれて見通しは良くなったと思います。
-しかし、このフォームには、バリデーションが動作しないという重大な問題があります
-VeeValidateのバリデーションはコンポーネント単位で実行されます。
-そのため、送信前の一括バリデーションのような処理を実装しようとすると、子コンポーネントのバリデータを何らかの手段で実行する必要があります。
+コード量は増えましたが、コンポーネントが細かく分かれて見通しは良くなったと思います。しかし、このフォームには、バリデーションが動作しないという重大な問題があります
+
+VeeValidateのバリデーションはコンポーネント単位で実行されます。そのため、送信前の一括バリデーションのような処理を実装しようとすると、子コンポーネントのバリデータを何らかの手段で実行する必要があります。
### EventBusパターン
-Vue.jsがコンポーネント間通信の方法として用意しているのが、イベントシステムです。このイベントシステムを活用することで、コンポーネントをまたがったバリデーションを実装することができます。この手法は、冒頭で記述したVeeValidateのドキュメントにも紹介されています。
+Vue.jsがコンポーネント間通信の方法として用意しているのが、[イベントシステム](https://jp.vuejs.org/v2/guide/events.html)です。こを活用することで、コンポーネントをまたがったバリデーションを実装することができます。この手法は、冒頭で記述したVeeValidateのドキュメントにも紹介されています。
http://vee-validate.logaretm.com/examples.html#event-bus-example
まず、イベントを伝達するバス(EventBus)として、Vueインスタンスを用意します。
```js
// EventBus.js
import Vue from 'vue';
export default new Vue();
```
次に、このイベントバスに`validate`イベントを発生させるコードを、バリデーションの呼び出し側に追加します。
```diff
diff --git a/src/components/EntryForm.vue b/src/components/EntryForm.vue
index db33dda..0dec669 100644
--- a/src/components/EntryForm.vue
+++ b/src/components/EntryForm.vue
@@ -10,6 +10,7 @@
</template>
<script>
/* eslint-disable no-console */
+ import EventBus from '../EventBus';
import MemberForm from './MemberForm';
import OptionForm from './OptionForm';
@@ -35,7 +36,10 @@
},
methods: {
submit() {
+ this.errors.clear();
this.$validator.validateAll();
+ EventBus.$emit('validate', this.errors);
+
if (this.errors.any()) {
console.log('cancel submission');
} else {
```
最後に、`validate`イベントに反応するコードを実装します。こちらは、コードの重複を避けるため[mixin](https://jp.vuejs.org/v2/guide/mixins.html)で実装すると良いでしょう。
```js
// mixins/NestedValidator.js
import EventBus from '../EventBus';
export default {
mounted() {
EventBus.$on('validate', (errorBag) => {
this.$validator.validateAll();
this.errors.items.forEach((error) => {
errorBag.add(error);
});
});
},
};
```
```diff
diff --git a/src/components/MemberForm.vue b/src/components/MemberForm.vue
index 392f9e2..fed96a5 100644
--- a/src/components/MemberForm.vue
+++ b/src/components/MemberForm.vue
@@ -43,8 +43,11 @@
</div>
</template>
<script>
+ import NestedValidator from '../mixins/NestedValidator';
+
export default {
name: 'member-form',
+ mixins: [NestedValidator],
props: [
'member',
],
```
```diff
diff --git a/src/components/OptionForm.vue b/src/components/OptionForm.vue
index da440a7..9f530d5 100644
--- a/src/components/OptionForm.vue
+++ b/src/components/OptionForm.vue
@@ -9,7 +9,7 @@
id="shipToDifferentAddress"
>
</div>
- <div v-show="option.shipToDifferentAddress">
+ <div v-if="option.shipToDifferentAddress">
<label for="shippingAddress">Shipping Address</label>
<input
type="text"
@@ -22,8 +22,11 @@
</div>
</template>
<script>
+ import NestedValidator from '../mixins/NestedValidator';
+
export default {
name: 'option-form',
+ mixins: [NestedValidator],
props: [
'option',
],
```
これで、従来と同様、バリデーションエラーが表示されるようになります!
## 別の設計方針
ここまで、親コンポーネントのバリデーション実行に合わせて、子コンポーネントのバリデーションを実行する、という設計で実装しました。
より柔軟な設計としては、以下のような方針も考えられます。
- 親コンポーネントは、1つ以上のバリデータを持つ
- 子コンポーネントは、任意のタイミングで自身のバリデータを親コンポーネントのバリデータに追加・削除できる
この方針の場合、以下のような実装になります。
```js
// EntryForm.vue
// バリデータのコレクションを用意し、バリデータの追加・削除イベントに対応するイベントハンドラを定義
created() {
this.validators = [this.$validator];
EventBus.$on('add-validator', (validator) => {
const index = this.validators.findIndex(v => v === validator);
if (index === -1) {
this.validators.push(validator);
}
});
EventBus.$on('remove-validator', (validator) => {
const index = this.validators.findIndex(v => v === validator);
if (index !== -1) {
this.validators.splice(index, 1);
}
});
},
methods: {
// 全てのバリデーションを実行するメソッド
validateAll() {
this.errors.clear();
this.validators.forEach((validator) => {
validator.validateAll();
validator.errors.items.forEach((error) => {
this.errors.add(error);
});
});
},
```
```js
// mixins/NestedValidator
created() {
// EntryForm.created() より後に実行するために、$nextTick()で1テンポ遅らせる
this.$nextTick(() => {
EventBus.$emit('add-validator', this.$validator);
});
},
// v-if でコンポーネントが消去されたときにバリデータを取り除く
beforeDestroy() {
EventBus.$emit('remove-validator', this.$validator);
},
```
VeeValidate自身は入れ子のフォームに対応する機能を持ちませんが、Vueのイベントを活用することで、入れ子のフォームでもバリデーションを機能させることができます。